diff --git a/bun.lock b/bun.lock
index 866807792b..57e3823b57 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
- "configVersion": 1,
"workspaces": {
"": {
"name": "@metorial/integrations-root",
@@ -2723,7 +2722,7 @@
},
"integrations/confluence": {
"name": "@slates-integrations/confluence",
- "version": "0.2.0-rc.7",
+ "version": "0.2.0-rc.8",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -4556,7 +4555,7 @@
},
"integrations/firebase": {
"name": "@slates-integrations/firebase",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -4967,7 +4966,7 @@
},
"integrations/gemini": {
"name": "@slates-integrations/gemini",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -5941,7 +5940,7 @@
},
"integrations/heroku": {
"name": "@slates-integrations/heroku",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -6414,7 +6413,7 @@
},
"integrations/instagram": {
"name": "@slates-integrations/instagram",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -6451,7 +6450,7 @@
},
"integrations/intercom": {
"name": "@slates-integrations/intercom",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -7439,7 +7438,7 @@
},
"integrations/mailchimp": {
"name": "@slates-integrations/mailchimp",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -7692,7 +7691,7 @@
},
"integrations/messenger": {
"name": "@slates-integrations/facebook-messenger",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -8175,7 +8174,7 @@
},
"integrations/netlify": {
"name": "@slates-integrations/netlify",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.7",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -9206,7 +9205,7 @@
},
"integrations/pipedrive": {
"name": "@slates-integrations/pipedrive",
- "version": "0.2.0-rc.6",
+ "version": "0.2.0-rc.7",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -9423,7 +9422,7 @@
},
"integrations/postgresql": {
"name": "@slates-integrations/postgresql",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -9739,7 +9738,7 @@
},
"integrations/quickbooks": {
"name": "@slates-integrations/quickbooks",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -9896,7 +9895,7 @@
},
"integrations/reddit": {
"name": "@slates-integrations/reddit",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -11440,19 +11439,6 @@
"typescript": "^5",
},
},
- "integrations/statbank-norway-ssb": {
- "name": "@slates-integrations/statbank-norway-ssb",
- "version": "0.1.0-rc.3",
- "dependencies": {
- "@lowerdeck/error": "^1.1.0",
- "@types/node": "^20",
- "slates": "1.0.0-rc.10",
- "zod": "^4.2",
- },
- "devDependencies": {
- "typescript": "^5",
- },
- },
"integrations/sslmate-cert-spotter-api": {
"name": "@slates-integrations/sslmate-cert-spotter-api",
"version": "0.2.0-rc.2",
@@ -11525,6 +11511,19 @@
"typescript": "^5",
},
},
+ "integrations/statbank-norway-ssb": {
+ "name": "@slates-integrations/statbank-norway-ssb",
+ "version": "0.1.0-rc.3",
+ "dependencies": {
+ "@lowerdeck/error": "^1.1.0",
+ "@types/node": "^20",
+ "slates": "1.0.0-rc.10",
+ "zod": "^4.2",
+ },
+ "devDependencies": {
+ "typescript": "^5",
+ },
+ },
"integrations/statuscake": {
"name": "@slates-integrations/statuscake",
"version": "0.2.0-rc.2",
@@ -11549,6 +11548,23 @@
"typescript": "^5",
},
},
+ "integrations/stern-financial-data": {
+ "name": "@slates-integrations/stern-financial-data",
+ "version": "0.1.0-rc.1",
+ "dependencies": {
+ "@lowerdeck/error": "^1.1.0",
+ "@mixmark-io/domino": "^2.2.0",
+ "@types/node": "^20",
+ "slates": "1.0.0-rc.10",
+ "xlsx": "^0.18.5",
+ "zod": "^4.2",
+ },
+ "devDependencies": {
+ "@slates/test": "1.0.0-rc.4",
+ "typescript": "^5",
+ "vitest": "^3.1.2",
+ },
+ },
"integrations/stitch": {
"name": "@slates-integrations/stitch",
"version": "0.2.0-rc.2",
@@ -12531,7 +12547,7 @@
},
"integrations/typeform": {
"name": "@slates-integrations/typeform",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.7",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -13267,7 +13283,7 @@
},
"integrations/xero": {
"name": "@slates-integrations/xero",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.8",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -13345,7 +13361,7 @@
},
"integrations/zapier": {
"name": "@slates-integrations/zapier",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.6",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -13540,7 +13556,7 @@
},
"integrations/zoom": {
"name": "@slates-integrations/zoom",
- "version": "0.2.0-rc.5",
+ "version": "0.2.0-rc.7",
"dependencies": {
"@lowerdeck/error": "^1.1.0",
"@slates/provider": "1.0.0-rc.11",
@@ -14305,6 +14321,8 @@
"@lowerdeck/validation": ["@lowerdeck/validation@1.0.10", "", {}, "sha512-M6F1+SRuxUKkgR9qPi3EhO5+WUXckAxhRnNYCtp1CbFl0HZSV+IIOdvfVq29RRn7TIObB2/rYvqJQ38tSHq6Pw=="],
+ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
+
"@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="],
"@rollup/plugin-alias": ["@rollup/plugin-alias@3.1.9", "", { "dependencies": { "slash": "^3.0.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw=="],
@@ -16255,8 +16273,6 @@
"@slates-integrations/squarespace": ["@slates-integrations/squarespace@workspace:integrations/squarespace"],
- "@slates-integrations/statbank-norway-ssb": ["@slates-integrations/statbank-norway-ssb@workspace:integrations/statbank-norway-ssb"],
-
"@slates-integrations/sslmate-cert-spotter-api": ["@slates-integrations/sslmate-cert-spotter-api@workspace:integrations/sslmate-cert-spotter-api"],
"@slates-integrations/stability-ai": ["@slates-integrations/stability-ai@workspace:integrations/stability-ai"],
@@ -16269,10 +16285,14 @@
"@slates-integrations/starton": ["@slates-integrations/starton@workspace:integrations/starton"],
+ "@slates-integrations/statbank-norway-ssb": ["@slates-integrations/statbank-norway-ssb@workspace:integrations/statbank-norway-ssb"],
+
"@slates-integrations/statuscake": ["@slates-integrations/statuscake@workspace:integrations/statuscake"],
"@slates-integrations/statuspage": ["@slates-integrations/statuspage@workspace:integrations/statuspage"],
+ "@slates-integrations/stern-financial-data": ["@slates-integrations/stern-financial-data@workspace:integrations/stern-financial-data"],
+
"@slates-integrations/stitch": ["@slates-integrations/stitch@workspace:integrations/stitch"],
"@slates-integrations/stoat": ["@slates-integrations/stoat@workspace:integrations/stoat"],
@@ -16777,6 +16797,8 @@
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+ "adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="],
+
"ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="],
"ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="],
@@ -16853,6 +16875,8 @@
"caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="],
+ "cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="],
+
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
"chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="],
@@ -16867,6 +16891,8 @@
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+ "codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="],
+
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -16893,6 +16919,8 @@
"cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
+ "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
+
"css-declaration-sorter": ["css-declaration-sorter@6.4.1", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g=="],
"css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="],
@@ -17007,6 +17035,8 @@
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+ "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="],
+
"fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
"fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="],
@@ -17453,6 +17483,8 @@
"sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="],
+ "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="],
+
"stable": ["stable@0.1.8", "", {}, "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
@@ -17567,10 +17599,16 @@
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+ "wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="],
+
+ "word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="],
+
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+ "xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="],
+
"xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -19521,6 +19559,8 @@
"@slates-integrations/statuspage/slates": ["slates@1.0.0-rc.9", "", { "dependencies": { "@slates/provider": "1.0.0-rc.10" } }, "sha512-fwuT9dcSmW6J/O84niSrj3X992XLtniaZR7XapimUKwmH/nZhHaiuglQoJ9WUi+P3cLv9R3SFqAMkjODTp5wpQ=="],
+ "@slates-integrations/stern-financial-data/@slates/test": ["@slates/test@1.0.0-rc.4", "", { "dependencies": { "@lowerdeck/testing-tools": "latest", "@slates/client": "1.0.0-rc.6", "@slates/profiles": "1.0.0-rc.4" } }, "sha512-gCh8IL15DvKiNiTjejm1KfifwtrPmMQ0xgQ4Hzk2/6PoaUBL7fzbKi1vLRiNd+7F7wvknnz04Y22HqpA++llgw=="],
+
"@slates-integrations/stitch/slates": ["slates@1.0.0-rc.9", "", { "dependencies": { "@slates/provider": "1.0.0-rc.10" } }, "sha512-fwuT9dcSmW6J/O84niSrj3X992XLtniaZR7XapimUKwmH/nZhHaiuglQoJ9WUi+P3cLv9R3SFqAMkjODTp5wpQ=="],
"@slates-integrations/stoat/slates": ["slates@1.0.0-rc.9", "", { "dependencies": { "@slates/provider": "1.0.0-rc.10" } }, "sha512-fwuT9dcSmW6J/O84niSrj3X992XLtniaZR7XapimUKwmH/nZhHaiuglQoJ9WUi+P3cLv9R3SFqAMkjODTp5wpQ=="],
@@ -21843,6 +21883,10 @@
"@slates-integrations/statuspage/slates/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="],
+ "@slates-integrations/stern-financial-data/@slates/test/@slates/client": ["@slates/client@1.0.0-rc.6", "", { "dependencies": { "@slates/proto": "1.0.0-rc.8", "@slates/provider": "1.0.0-rc.10", "@slates/provider-handler": "1.0.0-rc.9" } }, "sha512-y9DONyNvs5sxjebi0QkejwCmVV8msXdMidu+2M6rxCHKdq05L8WnxazpsDN+OxWSmrs9+fgISfZVlPKYl51LWw=="],
+
+ "@slates-integrations/stern-financial-data/@slates/test/@slates/profiles": ["@slates/profiles@1.0.0-rc.4", "", { "dependencies": { "@slates/client": "1.0.0-rc.6", "@slates/proto": "1.0.0-rc.8" } }, "sha512-VrBG1Fo1XmqMwPfaG6ZEikYp5nSDz4RJ7puRAAfhQLLM2Pf3kHtpb/NAiBmdJe9dz2UNLr0+wGzq/fyp61pURA=="],
+
"@slates-integrations/stitch/slates/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="],
"@slates-integrations/stoat/slates/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="],
@@ -22429,6 +22473,14 @@
"@slates-integrations/slack/@slates/test/@slates/profiles/@slates/proto": ["@slates/proto@1.0.0-rc.8", "", { "dependencies": { "@lowerdeck/emitter": "^1.0.4", "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-3PB2SCFi13mYNkkptX9dbz8xOFgEdtXd6atDtaaIDs8vSxIMWy8C9JoSqtqXDmJOfM8Jk/dCSWxjoTPEYnMfgQ=="],
+ "@slates-integrations/stern-financial-data/@slates/test/@slates/client/@slates/proto": ["@slates/proto@1.0.0-rc.8", "", { "dependencies": { "@lowerdeck/emitter": "^1.0.4", "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-3PB2SCFi13mYNkkptX9dbz8xOFgEdtXd6atDtaaIDs8vSxIMWy8C9JoSqtqXDmJOfM8Jk/dCSWxjoTPEYnMfgQ=="],
+
+ "@slates-integrations/stern-financial-data/@slates/test/@slates/client/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="],
+
+ "@slates-integrations/stern-financial-data/@slates/test/@slates/client/@slates/provider-handler": ["@slates/provider-handler@1.0.0-rc.9", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/proto": "1.0.0-rc.8", "@slates/provider": "1.0.0-rc.10", "zod": "^4.2.1" } }, "sha512-F63WHfzS050WLpZbQ/mRq4dRA7aHhcrahjvCluuH6rqwOASIo5aPf1QCPvktvTA7ufxfnscZe0uKiW3IFtsuzA=="],
+
+ "@slates-integrations/stern-financial-data/@slates/test/@slates/profiles/@slates/proto": ["@slates/proto@1.0.0-rc.8", "", { "dependencies": { "@lowerdeck/emitter": "^1.0.4", "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-3PB2SCFi13mYNkkptX9dbz8xOFgEdtXd6atDtaaIDs8vSxIMWy8C9JoSqtqXDmJOfM8Jk/dCSWxjoTPEYnMfgQ=="],
+
"@slates-integrations/youtube-analytics/@slates/test/@slates/client/@slates/proto": ["@slates/proto@1.0.0-rc.8", "", { "dependencies": { "@lowerdeck/emitter": "^1.0.4", "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-3PB2SCFi13mYNkkptX9dbz8xOFgEdtXd6atDtaaIDs8vSxIMWy8C9JoSqtqXDmJOfM8Jk/dCSWxjoTPEYnMfgQ=="],
"@slates-integrations/youtube-analytics/@slates/test/@slates/client/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="],
diff --git a/integrations/confluence/package.json b/integrations/confluence/package.json
index d013b5d34d..4b574c2142 100644
--- a/integrations/confluence/package.json
+++ b/integrations/confluence/package.json
@@ -15,5 +15,5 @@
"devDependencies": {
"typescript": "^5"
},
- "version": "0.2.0-rc.7"
+ "version": "0.2.0-rc.8"
}
diff --git a/integrations/confluence/src/lib/client.ts b/integrations/confluence/src/lib/client.ts
index 70a1e5de9e..d9e0054790 100644
--- a/integrations/confluence/src/lib/client.ts
+++ b/integrations/confluence/src/lib/client.ts
@@ -28,7 +28,7 @@ export interface ConfluencePage {
status: string;
title: string;
spaceId?: string;
- parentId?: string;
+ parentId?: string | null;
authorId?: string;
createdAt?: string;
version?: { number: number; message?: string; createdAt?: string };
diff --git a/integrations/confluence/src/tools/get-page.ts b/integrations/confluence/src/tools/get-page.ts
index 2a188c9f27..3aa0f889cd 100644
--- a/integrations/confluence/src/tools/get-page.ts
+++ b/integrations/confluence/src/tools/get-page.ts
@@ -47,7 +47,7 @@ export let getPage = SlateTool.create(spec, {
title: page.title,
status: page.status,
spaceId: page.spaceId,
- parentId: page.parentId,
+ parentId: page.parentId ?? undefined,
authorId: page.authorId,
createdAt: page.createdAt,
versionNumber: page.version?.number,
diff --git a/integrations/confluence/src/tools/list-pages.ts b/integrations/confluence/src/tools/list-pages.ts
index be75e6d850..7e108058a2 100644
--- a/integrations/confluence/src/tools/list-pages.ts
+++ b/integrations/confluence/src/tools/list-pages.ts
@@ -67,7 +67,7 @@ export let listPages = SlateTool.create(spec, {
title: p.title,
status: p.status,
spaceId: p.spaceId,
- parentId: p.parentId,
+ parentId: p.parentId ?? undefined,
versionNumber: p.version?.number,
createdAt: p.createdAt
}));
diff --git a/integrations/stern-financial-data/README.md b/integrations/stern-financial-data/README.md
new file mode 100644
index 0000000000..deb2a4ca16
--- /dev/null
+++ b/integrations/stern-financial-data/README.md
@@ -0,0 +1,31 @@
+#
Stern Financial Data
+
+Query public NYU Stern financial datasets from Aswath Damodaran's data pages.
+
+This integration reads public workbook datasets and falls back to the matching
+HTML table when workbook extraction fails. No authentication is required.
+
+## Tools
+
+- `list_sources`: list supported Stern data sources, fields, source URLs, and filter hints.
+- `get_source`: retrieve one source and return filtered rows. Full output is available
+ with `returnAll`, but filters and limits are recommended because rows are wide.
+
+## Sources
+
+- `erp`: country equity risk premiums, default spreads, corporate tax rates, and CDS data.
+- `us_industry_betas`: US industry beta, leverage, tax, cash, risk, and volatility data.
+- `global_industry_betas`: global industry beta, leverage, tax, cash, risk, and volatility data.
+
+## Authentication
+
+No authentication is required. The package uses `addNone()` and calls the public
+Stern pages and workbooks directly.
+
+## License
+
+This integration is licensed under the [FSL-1.1](https://github.com/metorial/metorial-platform/blob/dev/LICENSE).
+
+
diff --git a/integrations/stern-financial-data/docs/SPEC.md b/integrations/stern-financial-data/docs/SPEC.md
new file mode 100644
index 0000000000..568ca16159
--- /dev/null
+++ b/integrations/stern-financial-data/docs/SPEC.md
@@ -0,0 +1,67 @@
+# Slates Specification for Stern Financial Data
+
+## Overview
+
+Stern Financial Data reads public datasets from NYU Stern's Aswath Damodaran
+data pages. The integration focuses on compact discovery and filtered reads for
+large financial tables.
+
+Supported sources:
+
+- `erp`: country equity risk premium data from `ctryprem`.
+- `us_industry_betas`: US industry beta data from `betas`.
+- `global_industry_betas`: global industry beta data from `betaGlobal`.
+
+## Authentication
+
+These Stern datasets are public. No API key, OAuth flow, token, or account
+configuration is needed. The integration uses the no-auth Slate authentication
+method.
+
+## Extraction
+
+Each source has a public HTML page and workbook URL. `get_source` retrieves the
+HTML page, then tries the workbook first because workbook cells preserve numeric
+values and formatting. If workbook fetch or parsing fails, the tool falls back
+to parsing the HTML table with the same header aliases and type coercion.
+
+Percent values are returned as decimal numbers, for example `4.23%` becomes
+`0.0423`. Original cell text can be included with `includeRaw`.
+
+## Tools
+
+### list_sources
+
+Returns the supported source ids, source titles, page/workbook URLs, row fields,
+and supported filters.
+
+### get_source
+
+Retrieves one source and applies source-specific filters.
+
+ERP filters:
+
+- exact country list or country text search
+- Moody's rating list
+- equity risk premium range
+- country risk premium range
+- corporate tax rate range
+- sovereign CDS presence
+
+Industry beta filters:
+
+- exact industry list or industry text search
+- row type: `industry` or `aggregate`
+- beta range
+- unlevered beta range
+- minimum number of firms
+- maximum debt-to-equity ratio
+
+By default the tool returns up to 25 filtered rows. Use `returnAll: true` only
+when full output is needed; filtered reads are preferred because rows are wide
+and include many financial metrics.
+
+## Events
+
+The provider does not support webhooks or event subscriptions for these public
+datasets. This integration is read-only and tool-only.
diff --git a/integrations/stern-financial-data/logo.svg b/integrations/stern-financial-data/logo.svg
new file mode 100644
index 0000000000..abdc0a9854
--- /dev/null
+++ b/integrations/stern-financial-data/logo.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/integrations/stern-financial-data/package.json b/integrations/stern-financial-data/package.json
new file mode 100644
index 0000000000..b32ada5881
--- /dev/null
+++ b/integrations/stern-financial-data/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@slates-integrations/stern-financial-data",
+ "main": "src/index.ts",
+ "type": "module",
+ "scripts": {
+ "build": "bunx @vercel/ncc build src/index.ts -o dist -m -s",
+ "test": "vitest run --passWithNoTests",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@lowerdeck/error": "^1.1.0",
+ "@mixmark-io/domino": "^2.2.0",
+ "@types/node": "^20",
+ "slates": "1.0.0-rc.10",
+ "xlsx": "^0.18.5",
+ "zod": "^4.2"
+ },
+ "devDependencies": {
+ "@slates/test": "1.0.0-rc.4",
+ "typescript": "^5",
+ "vitest": "^3.1.2"
+ },
+ "version": "0.1.0-rc.1"
+}
diff --git a/integrations/stern-financial-data/slate.json b/integrations/stern-financial-data/slate.json
new file mode 100644
index 0000000000..1dec9090bb
--- /dev/null
+++ b/integrations/stern-financial-data/slate.json
@@ -0,0 +1,13 @@
+{
+ "name": "@metorial/stern-financial-data",
+ "description": "Query public NYU Stern financial datasets including country equity risk premiums and industry betas.",
+ "categories": ["finance-and-accounting", "data-and-analytics"],
+ "skills": [
+ "list NYU Stern financial datasets",
+ "query country equity risk premiums",
+ "filter country risk premium data",
+ "query US industry betas",
+ "query global industry betas",
+ "filter industry beta datasets"
+ ]
+}
diff --git a/integrations/stern-financial-data/src/auth.ts b/integrations/stern-financial-data/src/auth.ts
new file mode 100644
index 0000000000..0368388dae
--- /dev/null
+++ b/integrations/stern-financial-data/src/auth.ts
@@ -0,0 +1,4 @@
+import { SlateAuth } from 'slates';
+import { z } from 'zod';
+
+export let auth = SlateAuth.create().output(z.object({})).addNone();
diff --git a/integrations/stern-financial-data/src/config.ts b/integrations/stern-financial-data/src/config.ts
new file mode 100644
index 0000000000..f32a0aed8f
--- /dev/null
+++ b/integrations/stern-financial-data/src/config.ts
@@ -0,0 +1,4 @@
+import { SlateConfig } from 'slates';
+import { z } from 'zod';
+
+export let config = SlateConfig.create(z.object({}));
diff --git a/integrations/stern-financial-data/src/index.ts b/integrations/stern-financial-data/src/index.ts
new file mode 100644
index 0000000000..049865f875
--- /dev/null
+++ b/integrations/stern-financial-data/src/index.ts
@@ -0,0 +1,9 @@
+import { Slate } from 'slates';
+import { spec } from './spec';
+import { getSource, listSources } from './tools';
+
+export let provider = Slate.create({
+ spec,
+ tools: [listSources, getSource],
+ triggers: []
+});
diff --git a/integrations/stern-financial-data/src/lib/client.ts b/integrations/stern-financial-data/src/lib/client.ts
new file mode 100644
index 0000000000..db29b8ff77
--- /dev/null
+++ b/integrations/stern-financial-data/src/lib/client.ts
@@ -0,0 +1,111 @@
+import { sternFinancialDataApiError, sternFinancialDataServiceError } from './errors';
+import { extractRowsForSource, parseWorkbook } from './extractor';
+import { SOURCES, SourceId, SourceType, SternRow } from './sources';
+
+export type SourceResult = {
+ metadata: {
+ source: SourceId;
+ title: string;
+ pageUrl: string;
+ workbookUrl: string;
+ retrievedAt: string;
+ sourceType: SourceType;
+ workbookFallbackReason?: string;
+ };
+ rows: SternRow[];
+};
+
+let FETCH_TIMEOUT_MS = 20_000;
+
+let fetchWithTimeout = async (url: string, operation: string) => {
+ let controller = new AbortController();
+ let timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
+
+ try {
+ return await fetch(url, { signal: controller.signal });
+ } catch (error) {
+ if (controller.signal.aborted) {
+ throw sternFinancialDataServiceError(
+ `Stern Financial Data ${operation} timed out after ${FETCH_TIMEOUT_MS}ms.`
+ );
+ }
+
+ throw error;
+ } finally {
+ clearTimeout(timeout);
+ }
+};
+
+let fetchText = async (url: string) => {
+ try {
+ let response = await fetchWithTimeout(url, 'HTML page fetch');
+
+ if (!response.ok) {
+ throw Object.assign(new Error(`${response.status} ${response.statusText}`), {
+ status: response.status,
+ statusText: response.statusText
+ });
+ }
+
+ return await response.text();
+ } catch (error) {
+ throw sternFinancialDataApiError(error, 'HTML page fetch');
+ }
+};
+
+let fetchBuffer = async (url: string) => {
+ try {
+ let response = await fetchWithTimeout(url, 'workbook fetch');
+
+ if (!response.ok) {
+ throw Object.assign(new Error(`${response.status} ${response.statusText}`), {
+ status: response.status,
+ statusText: response.statusText
+ });
+ }
+
+ return Buffer.from(await response.arrayBuffer());
+ } catch (error) {
+ throw sternFinancialDataApiError(error, 'workbook fetch');
+ }
+};
+
+let errorMessage = (error: unknown) =>
+ error instanceof Error ? error.message : String(error);
+
+export class SternFinancialDataClient {
+ async getSource(sourceId: SourceId): Promise {
+ let source = SOURCES[sourceId];
+ let retrievedAt = new Date().toISOString();
+ let pageHtml = await fetchText(source.pageUrl);
+ let rows: SternRow[] = [];
+ let sourceType: SourceType = 'workbook';
+ let workbookFallbackReason: string | undefined;
+
+ try {
+ let workbook = parseWorkbook(await fetchBuffer(source.workbookUrl));
+ rows = extractRowsForSource(sourceId, workbook, pageHtml);
+ } catch (error) {
+ workbookFallbackReason = errorMessage(error);
+ sourceType = 'html';
+ rows = extractRowsForSource(sourceId, null, pageHtml);
+ }
+
+ if (rows.length === 0) {
+ throw sternFinancialDataServiceError(`No rows extracted for Stern source ${sourceId}.`);
+ }
+
+ return {
+ metadata: {
+ source: sourceId,
+ title: source.title,
+ pageUrl: source.pageUrl,
+ workbookUrl: source.workbookUrl,
+ retrievedAt,
+ sourceType,
+ workbookFallbackReason
+ },
+ rows
+ };
+ }
+}
diff --git a/integrations/stern-financial-data/src/lib/errors.ts b/integrations/stern-financial-data/src/lib/errors.ts
new file mode 100644
index 0000000000..62278e5c45
--- /dev/null
+++ b/integrations/stern-financial-data/src/lib/errors.ts
@@ -0,0 +1,93 @@
+import { ServiceError, badRequestError } from '@lowerdeck/error';
+
+type ErrorResponse = {
+ status?: number;
+ statusText?: string;
+ data?: unknown;
+};
+
+let isRecord = (value: unknown): value is Record =>
+ typeof value === 'object' && value !== null;
+
+let addMessage = (messages: string[], value: unknown) => {
+ if (typeof value !== 'string') return;
+
+ let trimmed = value.trim();
+ if (trimmed && !messages.includes(trimmed)) {
+ messages.push(trimmed);
+ }
+};
+
+let collectMessages = (value: unknown, messages: string[]) => {
+ if (!isRecord(value)) {
+ addMessage(messages, value);
+ return;
+ }
+
+ for (let key of ['message', 'error', 'detail', 'description', 'title']) {
+ addMessage(messages, value[key]);
+ }
+
+ if (Array.isArray(value.errors)) {
+ for (let item of value.errors) {
+ collectMessages(item, messages);
+ }
+ }
+};
+
+let extractMessage = (error: unknown) => {
+ let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined;
+ let messages: string[] = [];
+
+ collectMessages(response?.data, messages);
+
+ if (isRecord(error)) {
+ collectMessages(error.data, messages);
+ }
+
+ if (messages.length > 0) {
+ return messages.join(' - ');
+ }
+
+ if (error instanceof Error && error.message) {
+ return error.message;
+ }
+
+ return 'Unknown error';
+};
+
+let upstreamStatus = (error: unknown) => {
+ let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined;
+ return (
+ response?.status ??
+ (isRecord(error) && typeof error.status === 'number' ? error.status : undefined)
+ );
+};
+
+export let sternFinancialDataServiceError = (message: string) =>
+ new ServiceError(badRequestError({ message }));
+
+export let sternFinancialDataApiError = (error: unknown, operation = 'request') => {
+ if (error instanceof ServiceError) {
+ return error;
+ }
+
+ let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined;
+ let status = upstreamStatus(error);
+ let statusText = response?.statusText;
+ let statusLabel =
+ status !== undefined ? `HTTP ${status}${statusText ? ` ${statusText}` : ''}: ` : '';
+
+ let serviceError = sternFinancialDataServiceError(
+ `Stern Financial Data ${operation} failed: ${statusLabel}${extractMessage(error)}`
+ );
+
+ serviceError.data.reason = 'stern_financial_data_error';
+ serviceError.data.upstreamStatus = status;
+
+ if (error instanceof Error) {
+ serviceError.setParent(error);
+ }
+
+ return serviceError;
+};
diff --git a/integrations/stern-financial-data/src/lib/extractor.test.ts b/integrations/stern-financial-data/src/lib/extractor.test.ts
new file mode 100644
index 0000000000..daede40229
--- /dev/null
+++ b/integrations/stern-financial-data/src/lib/extractor.test.ts
@@ -0,0 +1,113 @@
+import { describe, expect, it } from 'vitest';
+import { extractRowsForSource, findHeaderMap, parseTypedValue } from './extractor';
+import { BETA_FIELDS, BetaRow, ERP_FIELDS, ErpRow } from './sources';
+
+describe('Stern financial data extractor', () => {
+ it('parses typed values and header aliases', () => {
+ expect(parseTypedValue('4.23%', undefined, 'number')).toBe(0.0423);
+ expect(parseTypedValue('1,207', undefined, 'integer')).toBe(1207);
+ expect(parseTypedValue('NA', undefined, 'number')).toBeNull();
+
+ expect(
+ findHeaderMap(
+ [
+ 'Country',
+ "Moody's rating",
+ 'Adj. Default Spread',
+ 'Country Risk Premium',
+ 'Equity Risk Premium',
+ 'Corporate Tax Rate',
+ 'Sovereignn CDS',
+ 'ERP based on sovereign CDSS'
+ ],
+ ERP_FIELDS
+ )
+ ).toEqual({
+ country: 0,
+ moodysRating: 1,
+ adjustedDefaultSpread: 2,
+ countryRiskPremium: 3,
+ equityRiskPremium: 4,
+ corporateTaxRate: 5,
+ sovereignCds: 6,
+ erpBasedOnSovereignCds: 7
+ });
+ });
+
+ it('parses ERP rows through the HTML fallback path', () => {
+ let rows = extractRowsForSource(
+ 'erp',
+ null,
+ '' +
+ "| Country | Moody's rating | Adj. Default Spread | Country Risk Premium | Equity Risk Premium | Corporate Tax Rate | Sovereignn CDS | ERP based on sovereign CDSS |
" +
+ '| United States | Aa1 | 0.23% | 0.23% | 4.46% | 25.00% | 0.30% | 4.69% |
' +
+ '
'
+ ) as ErpRow[];
+
+ expect(rows).toHaveLength(1);
+ expect(rows[0]).toMatchObject({
+ country: 'United States',
+ moodysRating: 'Aa1',
+ adjustedDefaultSpread: 0.0023,
+ countryRiskPremium: 0.0023,
+ equityRiskPremium: 0.0446,
+ corporateTaxRate: 0.25,
+ sovereignCds: 0.003,
+ erpBasedOnSovereignCds: 0.0469
+ });
+ expect(rows[0]?.raw.adjustedDefaultSpread).toBe('0.23%');
+ });
+
+ it('parses US industry beta rows through the HTML fallback path', () => {
+ let rows = extractRowsForSource(
+ 'us_industry_betas',
+ null,
+ '' +
+ '| Industry Name | Number of firms | Beta | D/E Ratio | Effective Tax rate | Unlevered beta | Cash/Firm value | Unlevered beta corrected for cash | HiLo Risk | Standard deviation of equity | Standard deviation in operating income (last 10 years) |
' +
+ '| Advertising | 52 | 1.21 | 40.20% | 5.02% | 0.93 | 7.73% | 1.01 | 0.6233 | 62.91% | 15.17% |
' +
+ '| Total Market | 5,994 | 1.04 | 51.48% | 13.31% | 0.75 | 9.70% | 0.83 | 0.3691 | 42.07% | 24.29% |
' +
+ '
'
+ ) as BetaRow[];
+
+ expect(rows).toHaveLength(2);
+ expect(rows[0]).toMatchObject({
+ industryName: 'Advertising',
+ numberOfFirms: 52,
+ beta: 1.21,
+ debtToEquityRatio: 0.402,
+ effectiveTaxRate: 0.0502,
+ rowType: 'industry'
+ });
+ expect(rows[1]).toMatchObject({
+ industryName: 'Total Market',
+ numberOfFirms: 5994,
+ rowType: 'aggregate'
+ });
+ });
+
+ it('parses global industry beta rows through the HTML fallback path', () => {
+ let rows = extractRowsForSource(
+ 'global_industry_betas',
+ null,
+ '' +
+ '' +
+ '| Industry Name | Number of firms | Beta | D/E Ratio | Effective Tax rate | Unlevered beta | Cash/Firm value | Unlevered beta corrected for cash | HiLo Risk | Standard deviation of equity | Standard deviation in operating income (last 10 years) |
' +
+ '| Semiconductor Equip | 391 | 2.13 | 10.55% | 14.40% | 1.94 | 12.00% | 2.20 | 0.5678 | 58.11% | 26.22% |
' +
+ '| Total Market (without financials) | 43,056 | 1.08 | 25.95% | 13.13% | 0.90 | 5.52% | 0.96 | 0.3765 | 43.02% | 24.86% |
' +
+ '
'
+ ) as BetaRow[];
+
+ expect(rows).toHaveLength(2);
+ expect(rows[0]).toMatchObject({
+ industryName: 'Semiconductor Equip',
+ numberOfFirms: 391,
+ beta: 2.13,
+ debtToEquityRatio: 0.1055,
+ rowType: 'industry'
+ });
+ expect(rows[1]).toMatchObject({
+ industryName: 'Total Market (without financials)',
+ rowType: 'aggregate'
+ });
+ });
+});
diff --git a/integrations/stern-financial-data/src/lib/extractor.ts b/integrations/stern-financial-data/src/lib/extractor.ts
new file mode 100644
index 0000000000..c4e5208e18
--- /dev/null
+++ b/integrations/stern-financial-data/src/lib/extractor.ts
@@ -0,0 +1,718 @@
+import * as domino from '@mixmark-io/domino';
+import * as XLSX from 'xlsx';
+import { sternFinancialDataServiceError } from './errors';
+import {
+ BETA_FIELDS,
+ BetaField,
+ BetaRow,
+ ERP_FIELDS,
+ ErpField,
+ ErpRow,
+ SOURCES,
+ SourceId,
+ SternRow
+} from './sources';
+
+type Cell = {
+ text: string;
+ value?: unknown;
+};
+
+type TypedValueKind = 'string' | 'integer' | 'number';
+type HeaderMap = Record;
+type Workbook = XLSX.WorkBook;
+type Mapper = (
+ cells: Cell[],
+ headerMap: HeaderMap
+) => TRow | null;
+
+let HEADER_ALIASES: Record = {
+ country: ['country'],
+ moodysRating: ["moody's rating", 'moodys rating', 'moody rating'],
+ adjustedDefaultSpread: [
+ 'adj. default spread',
+ 'adj default spread',
+ 'adjusted default spread',
+ 'rating-based default spread',
+ 'rating based default spread',
+ 'default spread'
+ ],
+ countryRiskPremium: ['country risk premium', 'crp'],
+ equityRiskPremium: ['equity risk premium', 'total equity risk premium', 'erp', 'final erp'],
+ corporateTaxRate: ['corporate tax rate', 'tax rate'],
+ sovereignCds: [
+ 'sovereign cds',
+ 'sovereignn cds',
+ 'sovereign cds, net of swiss cds',
+ 'sovereign cds net of swiss cds'
+ ],
+ erpBasedOnSovereignCds: [
+ 'erp based on sovereign cds',
+ 'erp based on sovereign cdss',
+ 'total equity risk premium2'
+ ],
+ industryName: ['industry name'],
+ numberOfFirms: ['number of firms'],
+ beta: ['beta'],
+ debtToEquityRatio: ['d/e ratio', 'de ratio', 'debt/equity ratio', 'debt to equity ratio'],
+ effectiveTaxRate: ['effective tax rate'],
+ unleveredBeta: ['unlevered beta'],
+ cashToFirmValue: ['cash/firm value', 'cash firm value'],
+ unleveredBetaCorrectedForCash: ['unlevered beta corrected for cash'],
+ hiloRisk: ['hilo risk', 'hi lo risk'],
+ standardDeviationOfEquity: [
+ 'standard deviation of equity',
+ 'standard deviation equity',
+ 'standard deviation (equity)'
+ ],
+ standardDeviationInOperatingIncomeLast10Years: [
+ 'standard deviation in operating income (last 10 years)',
+ 'standard deviation in operating income last 10 years',
+ 'standard deviation operating income'
+ ]
+};
+
+let UNAVAILABLE_VALUES = new Set(['', 'na', 'n/a', 'n.a.', 'nm', 'unavailable', '-', '—']);
+
+export let normalizeText = (value: unknown) => {
+ if (value === null || value === undefined) {
+ return '';
+ }
+
+ return String(value)
+ .replace(/\u00a0/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+};
+
+export let normalizeHeader = (value: unknown) =>
+ normalizeText(value)
+ .toLowerCase()
+ .replace(/&/g, ' and ')
+ .replace(/[’']/g, '')
+ .replace(/[^a-z0-9]+/g, ' ')
+ .replace(/\s+/g, ' ')
+ .trim();
+
+let isUnavailable = (value: unknown) =>
+ UNAVAILABLE_VALUES.has(normalizeText(value).toLowerCase());
+
+let coerceNumberText = (value: unknown) =>
+ normalizeText(value).replace(/[%,$]/g, '').replace(/,/g, '').replace(/[()]/g, '').trim();
+
+let cleanNumber = (value: number) =>
+ Number.isFinite(value) ? Number(value.toFixed(12)) : null;
+
+export let parseTypedValue = (
+ text: unknown,
+ rawValue: unknown,
+ kind: TypedValueKind
+): string | number | null => {
+ let normalized = normalizeText(text);
+
+ if (isUnavailable(normalized)) {
+ return null;
+ }
+
+ if (kind === 'string') {
+ return normalized || null;
+ }
+
+ if (kind === 'integer') {
+ if (typeof rawValue === 'number' && !/%/.test(normalized)) {
+ return Math.round(rawValue);
+ }
+
+ let intValue = Number.parseInt(coerceNumberText(normalized), 10);
+ return Number.isFinite(intValue) ? intValue : null;
+ }
+
+ if (/%/.test(normalized)) {
+ let percentValue = Number.parseFloat(coerceNumberText(normalized));
+ return cleanNumber(percentValue / 100);
+ }
+
+ if (typeof rawValue === 'number') {
+ return cleanNumber(rawValue);
+ }
+
+ let numberValue = Number.parseFloat(coerceNumberText(normalized));
+ return cleanNumber(numberValue);
+};
+
+let parseStringValue = (text: unknown, rawValue: unknown) =>
+ parseTypedValue(text, rawValue, 'string') as string | null;
+
+let parseNumberValue = (text: unknown, rawValue: unknown) =>
+ parseTypedValue(text, rawValue, 'number') as number | null;
+
+let parseIntegerValue = (text: unknown, rawValue: unknown) =>
+ parseTypedValue(text, rawValue, 'integer') as number | null;
+
+let compileAliases = (field: string) =>
+ (HEADER_ALIASES[field] ?? [field]).map(normalizeHeader);
+
+let headerMatchesField = (header: string, field: string) =>
+ compileAliases(field).includes(normalizeHeader(header));
+
+export let findHeaderMap = (
+ headers: readonly string[],
+ requiredFields: readonly T[]
+) => {
+ let map: Partial> = {};
+
+ headers.forEach((header, index) => {
+ for (let field of requiredFields) {
+ if (map[field] === undefined && headerMatchesField(header, field)) {
+ map[field] = index;
+ }
+ }
+ });
+
+ let missing = requiredFields.filter(field => map[field] === undefined);
+ return missing.length > 0 ? null : (map as HeaderMap);
+};
+
+let hasAnyContent = (cells: readonly Cell[]) => cells.some(cell => normalizeText(cell.text));
+
+let trimTrailingEmptyCells = (cells: Cell[]) => {
+ let end = cells.length;
+
+ while (end > 0 && !normalizeText(cells[end - 1]?.text)) {
+ end -= 1;
+ }
+
+ return cells.slice(0, end);
+};
+
+let cellAt = (cells: readonly Cell[], index: number) =>
+ index >= 0 ? cells[index] : undefined;
+
+let getRawValue = (cells: readonly Cell[], index: number) => cellAt(cells, index)?.value;
+
+let makeRaw = (
+ fields: readonly T[],
+ cells: readonly Cell[],
+ headerMap: HeaderMap
+) =>
+ fields.reduce(
+ (raw, field) => {
+ raw[field] = normalizeText(cellAt(cells, headerMap[field])?.text);
+ return raw;
+ },
+ {} as Record
+ );
+
+let mapErpRow = (cells: Cell[], headerMap: HeaderMap): ErpRow | null => {
+ let raw = makeRaw(ERP_FIELDS, cells, headerMap);
+ let country = raw.country;
+
+ if (!country || /^country$/i.test(country) || /^frontier markets/i.test(country)) {
+ return null;
+ }
+
+ return {
+ country: parseStringValue(raw.country, getRawValue(cells, headerMap.country)) ?? country,
+ moodysRating: parseStringValue(
+ raw.moodysRating,
+ getRawValue(cells, headerMap.moodysRating)
+ ),
+ adjustedDefaultSpread: parseNumberValue(
+ raw.adjustedDefaultSpread,
+ getRawValue(cells, headerMap.adjustedDefaultSpread)
+ ),
+ countryRiskPremium: parseNumberValue(
+ raw.countryRiskPremium,
+ getRawValue(cells, headerMap.countryRiskPremium)
+ ),
+ equityRiskPremium: parseNumberValue(
+ raw.equityRiskPremium,
+ getRawValue(cells, headerMap.equityRiskPremium)
+ ),
+ corporateTaxRate: parseNumberValue(
+ raw.corporateTaxRate,
+ getRawValue(cells, headerMap.corporateTaxRate)
+ ),
+ sovereignCds: parseNumberValue(
+ raw.sovereignCds,
+ getRawValue(cells, headerMap.sovereignCds)
+ ),
+ erpBasedOnSovereignCds: parseNumberValue(
+ raw.erpBasedOnSovereignCds,
+ getRawValue(cells, headerMap.erpBasedOnSovereignCds)
+ ),
+ raw
+ };
+};
+
+export let classifyBetaRow = (industryName: string) =>
+ /^total market/i.test(normalizeText(industryName)) ? 'aggregate' : 'industry';
+
+let mapBetaRow = (cells: Cell[], headerMap: HeaderMap): BetaRow | null => {
+ let raw = makeRaw(BETA_FIELDS, cells, headerMap);
+ let industryName = raw.industryName;
+
+ if (!industryName || /^industry name$/i.test(industryName)) {
+ return null;
+ }
+
+ return {
+ industryName:
+ parseStringValue(raw.industryName, getRawValue(cells, headerMap.industryName)) ??
+ industryName,
+ numberOfFirms: parseIntegerValue(
+ raw.numberOfFirms,
+ getRawValue(cells, headerMap.numberOfFirms)
+ ),
+ beta: parseNumberValue(raw.beta, getRawValue(cells, headerMap.beta)),
+ debtToEquityRatio: parseNumberValue(
+ raw.debtToEquityRatio,
+ getRawValue(cells, headerMap.debtToEquityRatio)
+ ),
+ effectiveTaxRate: parseNumberValue(
+ raw.effectiveTaxRate,
+ getRawValue(cells, headerMap.effectiveTaxRate)
+ ),
+ unleveredBeta: parseNumberValue(
+ raw.unleveredBeta,
+ getRawValue(cells, headerMap.unleveredBeta)
+ ),
+ cashToFirmValue: parseNumberValue(
+ raw.cashToFirmValue,
+ getRawValue(cells, headerMap.cashToFirmValue)
+ ),
+ unleveredBetaCorrectedForCash: parseNumberValue(
+ raw.unleveredBetaCorrectedForCash,
+ getRawValue(cells, headerMap.unleveredBetaCorrectedForCash)
+ ),
+ hiloRisk: parseNumberValue(raw.hiloRisk, getRawValue(cells, headerMap.hiloRisk)),
+ standardDeviationOfEquity: parseNumberValue(
+ raw.standardDeviationOfEquity,
+ getRawValue(cells, headerMap.standardDeviationOfEquity)
+ ),
+ standardDeviationInOperatingIncomeLast10Years: parseNumberValue(
+ raw.standardDeviationInOperatingIncomeLast10Years,
+ getRawValue(cells, headerMap.standardDeviationInOperatingIncomeLast10Years)
+ ),
+ rowType: classifyBetaRow(industryName),
+ raw
+ };
+};
+
+let makeCell = (text: unknown, value?: unknown): Cell => ({
+ text: normalizeText(text),
+ value
+});
+
+let sheetRows = (sheet: XLSX.WorkSheet | undefined) => {
+ if (!sheet || !sheet['!ref']) {
+ return [];
+ }
+
+ let range = XLSX.utils.decode_range(sheet['!ref']);
+ let rows: Cell[][] = [];
+
+ for (let rowIndex = range.s.r; rowIndex <= range.e.r; rowIndex += 1) {
+ let cells: Cell[] = [];
+
+ for (let colIndex = range.s.c; colIndex <= range.e.c; colIndex += 1) {
+ let address = XLSX.utils.encode_cell({ r: rowIndex, c: colIndex });
+ let cell = sheet[address];
+
+ cells.push(makeCell(cell ? (cell.w !== undefined ? cell.w : cell.v) : '', cell?.v));
+ }
+
+ cells = trimTrailingEmptyCells(cells);
+
+ if (hasAnyContent(cells)) {
+ rows.push(cells);
+ }
+ }
+
+ return rows;
+};
+
+export let parseWorkbook = (buffer: Buffer) =>
+ XLSX.read(buffer, { type: 'buffer', cellDates: true, cellNF: true });
+
+let findTableInRows = (
+ rows: Cell[][],
+ requiredFields: readonly T[]
+): { rows: Cell[][]; headerIndex: number; headerMap: HeaderMap } | null => {
+ for (let index = 0; index < rows.length; index += 1) {
+ let headers = rows[index]?.map(cell => cell.text) ?? [];
+ let headerMap = findHeaderMap(headers, requiredFields);
+
+ if (headerMap) {
+ return {
+ rows,
+ headerIndex: index,
+ headerMap
+ };
+ }
+ }
+
+ return null;
+};
+
+let findWorkbookTable = (
+ workbook: Workbook,
+ requiredFields: readonly T[]
+) => {
+ for (let sheetName of workbook.SheetNames) {
+ let rows = sheetRows(workbook.Sheets[sheetName]);
+ let table = findTableInRows(rows, requiredFields);
+
+ if (table) {
+ return {
+ ...table,
+ sheetName
+ };
+ }
+ }
+
+ return null;
+};
+
+let parseRowsAfterHeader = (
+ rows: Cell[][],
+ headerIndex: number,
+ headerMap: HeaderMap,
+ mapper: Mapper
+) => {
+ let parsed: TRow[] = [];
+
+ for (let index = headerIndex + 1; index < rows.length; index += 1) {
+ let cells = rows[index] ?? [];
+ let firstCell = normalizeText(cells[0]?.text);
+
+ if (!firstCell) {
+ break;
+ }
+
+ let row = mapper(cells, headerMap);
+
+ if (row) {
+ parsed.push(row);
+ }
+ }
+
+ return parsed;
+};
+
+export let extractBetaRowsFromWorkbook = (workbook: Workbook) => {
+ let table = findWorkbookTable(workbook, BETA_FIELDS);
+
+ if (!table) {
+ throw sternFinancialDataServiceError('Could not find beta table in workbook.');
+ }
+
+ return parseRowsAfterHeader(table.rows, table.headerIndex, table.headerMap, mapBetaRow);
+};
+
+let extractErpCountryRowsFromWorkbook = (workbook: Workbook) => {
+ let required = [
+ 'country',
+ 'moodysRating',
+ 'adjustedDefaultSpread',
+ 'countryRiskPremium',
+ 'equityRiskPremium',
+ 'sovereignCds',
+ 'erpBasedOnSovereignCds'
+ ] as const;
+ let table = findWorkbookTable(workbook, required);
+
+ if (!table) {
+ throw sternFinancialDataServiceError('Could not find country ERP table in workbook.');
+ }
+
+ let headerMap = {
+ ...table.headerMap,
+ corporateTaxRate: -1
+ } satisfies HeaderMap;
+
+ let rows: ErpRow[] = [];
+
+ for (let index = table.headerIndex + 1; index < table.rows.length; index += 1) {
+ let cells = table.rows[index] ?? [];
+ let country = normalizeText(cellAt(cells, headerMap.country)?.text);
+
+ if (!country || /^frontier markets/i.test(country)) {
+ break;
+ }
+
+ let row = mapErpRow(cells, headerMap);
+
+ if (row) {
+ rows.push(row);
+ }
+ }
+
+ return rows;
+};
+
+let findFrontierTable = (workbook: Workbook) => {
+ for (let sheetName of workbook.SheetNames) {
+ let rows = sheetRows(workbook.Sheets[sheetName]);
+
+ for (let index = 0; index < rows.length; index += 1) {
+ let headers = rows[index]?.map(cell => cell.text) ?? [];
+ let normalizedHeaders = headers.map(normalizeHeader);
+ let prsIndex = normalizedHeaders.indexOf('prs composite risk score');
+
+ if (prsIndex === -1) {
+ continue;
+ }
+
+ let headerMap = findHeaderMap(headers, [
+ 'country',
+ 'equityRiskPremium',
+ 'countryRiskPremium',
+ 'adjustedDefaultSpread'
+ ] as const);
+
+ if (headerMap) {
+ return {
+ rows,
+ headerIndex: index,
+ headerMap
+ };
+ }
+ }
+ }
+
+ return null;
+};
+
+let extractErpFrontierRowsFromWorkbook = (workbook: Workbook) => {
+ let table = findFrontierTable(workbook);
+
+ if (!table) {
+ return [];
+ }
+
+ let rows: ErpRow[] = [];
+
+ for (let index = table.headerIndex + 1; index < table.rows.length; index += 1) {
+ let cells = table.rows[index] ?? [];
+ let country = normalizeText(cellAt(cells, table.headerMap.country)?.text);
+
+ if (!country) {
+ break;
+ }
+
+ let adjustedDefaultSpreadCell = cellAt(cells, table.headerMap.adjustedDefaultSpread);
+ let countryRiskPremiumCell = cellAt(cells, table.headerMap.countryRiskPremium);
+ let equityRiskPremiumCell = cellAt(cells, table.headerMap.equityRiskPremium);
+
+ rows.push({
+ country,
+ moodysRating: 'NR',
+ adjustedDefaultSpread: parseNumberValue(
+ adjustedDefaultSpreadCell?.text,
+ adjustedDefaultSpreadCell?.value
+ ),
+ countryRiskPremium: parseNumberValue(
+ countryRiskPremiumCell?.text,
+ countryRiskPremiumCell?.value
+ ),
+ equityRiskPremium: parseNumberValue(
+ equityRiskPremiumCell?.text,
+ equityRiskPremiumCell?.value
+ ),
+ corporateTaxRate: null,
+ sovereignCds: null,
+ erpBasedOnSovereignCds: null,
+ raw: {
+ country,
+ moodysRating: 'NR',
+ adjustedDefaultSpread: normalizeText(adjustedDefaultSpreadCell?.text),
+ countryRiskPremium: normalizeText(countryRiskPremiumCell?.text),
+ equityRiskPremium: normalizeText(equityRiskPremiumCell?.text),
+ corporateTaxRate: '',
+ sovereignCds: '',
+ erpBasedOnSovereignCds: ''
+ }
+ });
+ }
+
+ return rows;
+};
+
+let extractCountryTaxRatesFromWorkbook = (workbook: Workbook) => {
+ let taxByCountry: Record = {};
+ let sheet = workbook.Sheets['PRS Worksheet'] ?? workbook.Sheets['Country Tax Rates'];
+
+ if (!sheet) {
+ return taxByCountry;
+ }
+
+ let rows = sheetRows(sheet);
+ let headerMap: HeaderMap<'country' | 'corporateTaxRate'> | null = null;
+ let headerIndex = -1;
+
+ for (let index = 0; index < rows.length; index += 1) {
+ headerMap = findHeaderMap(rows[index]?.map(cell => cell.text) ?? [], [
+ 'country',
+ 'corporateTaxRate'
+ ] as const);
+
+ if (headerMap) {
+ headerIndex = index;
+ break;
+ }
+ }
+
+ if (!headerMap) {
+ return taxByCountry;
+ }
+
+ for (let rowIndex = headerIndex + 1; rowIndex < rows.length; rowIndex += 1) {
+ let cells = rows[rowIndex] ?? [];
+ let countryCell = cellAt(cells, headerMap.country);
+ let taxCell = cellAt(cells, headerMap.corporateTaxRate);
+ let country = normalizeText(countryCell?.text);
+
+ if (!country) {
+ continue;
+ }
+
+ taxByCountry[country] = taxCell ?? makeCell('', undefined);
+ }
+
+ return taxByCountry;
+};
+
+export let extractErpRowsFromWorkbook = (workbook: Workbook) => {
+ let erpRows = extractErpCountryRowsFromWorkbook(workbook);
+ let frontierRows = extractErpFrontierRowsFromWorkbook(workbook);
+ let taxByCountry = extractCountryTaxRatesFromWorkbook(workbook);
+ let rows = erpRows.concat(frontierRows).map(row => {
+ let tax = taxByCountry[row.country];
+
+ if (tax) {
+ row.corporateTaxRate = parseNumberValue(tax.text, tax.value);
+ row.raw.corporateTaxRate = normalizeText(tax.text);
+ }
+
+ return row;
+ });
+
+ if (rows.length === 0) {
+ throw sternFinancialDataServiceError('Could not find ERP rows in workbook.');
+ }
+
+ return rows;
+};
+
+let removeNode = (node: any) => {
+ if (node?.parentNode) {
+ node.parentNode.removeChild(node);
+ }
+};
+
+let removeComments = (node: any) => {
+ let child = node?.firstChild;
+
+ while (child) {
+ let next = child.nextSibling;
+
+ if (child.nodeType === 8) {
+ removeNode(child);
+ } else {
+ removeComments(child);
+ }
+
+ child = next;
+ }
+};
+
+let isHidden = (element: any) => {
+ let current = element;
+
+ while (current?.getAttribute) {
+ let style = String(current.getAttribute('style') ?? '').toLowerCase();
+
+ if (
+ current.hasAttribute?.('hidden') ||
+ /display\s*:\s*none/.test(style) ||
+ /visibility\s*:\s*hidden/.test(style)
+ ) {
+ return true;
+ }
+
+ current = current.parentNode;
+ }
+
+ return false;
+};
+
+let toArray = (list: ArrayLike | null | undefined) => Array.from(list ?? []);
+
+let cleanDocument = (html: string) => {
+ let window = domino.createWindow(html);
+ let document = window.document;
+
+ toArray(
+ document.querySelectorAll('script, style, noscript, link, meta, title, colgroup, col')
+ ).forEach(removeNode);
+ removeComments(document);
+
+ return document;
+};
+
+let tableRowsFromHtml = (html: string) => {
+ let document = cleanDocument(html);
+ let tables = toArray(document.querySelectorAll('table'));
+
+ return tables.map((table: any) =>
+ toArray(table.querySelectorAll('tr'))
+ .filter((row: any) => !isHidden(row))
+ .map((row: any) =>
+ trimTrailingEmptyCells(
+ toArray(row.querySelectorAll('th,td'))
+ .filter((cell: any) => !isHidden(cell))
+ .map((cell: any) => makeCell(cell.textContent, undefined))
+ )
+ )
+ .filter(hasAnyContent)
+ );
+};
+
+let extractRowsFromHtml = (
+ html: string,
+ requiredFields: readonly TField[],
+ mapper: Mapper
+) => {
+ let tables = tableRowsFromHtml(html);
+
+ for (let tableRows of tables) {
+ let table = findTableInRows(tableRows, requiredFields);
+
+ if (table) {
+ return parseRowsAfterHeader(table.rows, table.headerIndex, table.headerMap, mapper);
+ }
+ }
+
+ throw sternFinancialDataServiceError('Could not find matching table in HTML.');
+};
+
+export let extractErpRowsFromHtml = (html: string) =>
+ extractRowsFromHtml(html, ERP_FIELDS, mapErpRow);
+
+export let extractBetaRowsFromHtml = (html: string) =>
+ extractRowsFromHtml(html, BETA_FIELDS, mapBetaRow);
+
+export let extractRowsForSource = (
+ sourceId: SourceId,
+ workbook: Workbook | null,
+ html: string
+) => {
+ let source = SOURCES[sourceId];
+
+ if (source.kind === 'erp') {
+ return workbook ? extractErpRowsFromWorkbook(workbook) : extractErpRowsFromHtml(html);
+ }
+
+ return workbook ? extractBetaRowsFromWorkbook(workbook) : extractBetaRowsFromHtml(html);
+};
diff --git a/integrations/stern-financial-data/src/lib/filters.ts b/integrations/stern-financial-data/src/lib/filters.ts
new file mode 100644
index 0000000000..4892d6602a
--- /dev/null
+++ b/integrations/stern-financial-data/src/lib/filters.ts
@@ -0,0 +1,128 @@
+import { BetaRow, BetaRowType, ErpRow, SourceId, SternRow } from './sources';
+
+export type CommonControls = {
+ limit: number;
+ offset: number;
+ returnAll: boolean;
+ includeRaw: boolean;
+};
+
+export type ErpFilters = CommonControls & {
+ source: 'erp';
+ countries?: string[];
+ countrySearch?: string;
+ moodysRatings?: string[];
+ minEquityRiskPremium?: number;
+ maxEquityRiskPremium?: number;
+ minCountryRiskPremium?: number;
+ maxCountryRiskPremium?: number;
+ minCorporateTaxRate?: number;
+ maxCorporateTaxRate?: number;
+ hasSovereignCds?: boolean;
+};
+
+export type BetaFilters = CommonControls & {
+ source: 'us_industry_betas' | 'global_industry_betas';
+ industries?: string[];
+ industrySearch?: string;
+ rowType?: BetaRowType;
+ minBeta?: number;
+ maxBeta?: number;
+ minUnleveredBeta?: number;
+ maxUnleveredBeta?: number;
+ minNumberOfFirms?: number;
+ maxDebtToEquityRatio?: number;
+};
+
+export type SourceFilters = ErpFilters | BetaFilters;
+
+export type PaginatedRows = {
+ filteredRows: SternRow[];
+ returnedRows: Array | SternRow>;
+ returnedRowCount: number;
+ filteredRowCount: number;
+ totalRowCount: number;
+ truncated: boolean;
+};
+
+let normalizeMatchText = (value: string) => value.trim().toLowerCase();
+
+let valueInList = (value: string | null | undefined, accepted: string[] | undefined) => {
+ if (!accepted?.length) return true;
+ if (!value) return false;
+
+ let normalizedValue = normalizeMatchText(value);
+ return accepted.some(entry => normalizeMatchText(entry) === normalizedValue);
+};
+
+let valueContains = (value: string | null | undefined, search: string | undefined) => {
+ if (!search) return true;
+ if (!value) return false;
+
+ return normalizeMatchText(value).includes(normalizeMatchText(search));
+};
+
+let numberAtLeast = (value: number | null, threshold: number | undefined) =>
+ threshold === undefined || (value !== null && value >= threshold);
+
+let numberAtMost = (value: number | null, threshold: number | undefined) =>
+ threshold === undefined || (value !== null && value <= threshold);
+
+let isErpRow = (row: SternRow): row is ErpRow => 'country' in row;
+let isBetaRow = (row: SternRow): row is BetaRow => 'industryName' in row;
+
+let filterErpRow = (row: ErpRow, filters: ErpFilters) =>
+ valueInList(row.country, filters.countries) &&
+ valueContains(row.country, filters.countrySearch) &&
+ valueInList(row.moodysRating, filters.moodysRatings) &&
+ numberAtLeast(row.equityRiskPremium, filters.minEquityRiskPremium) &&
+ numberAtMost(row.equityRiskPremium, filters.maxEquityRiskPremium) &&
+ numberAtLeast(row.countryRiskPremium, filters.minCountryRiskPremium) &&
+ numberAtMost(row.countryRiskPremium, filters.maxCountryRiskPremium) &&
+ numberAtLeast(row.corporateTaxRate, filters.minCorporateTaxRate) &&
+ numberAtMost(row.corporateTaxRate, filters.maxCorporateTaxRate) &&
+ (filters.hasSovereignCds === undefined ||
+ (filters.hasSovereignCds ? row.sovereignCds !== null : row.sovereignCds === null));
+
+let filterBetaRow = (row: BetaRow, filters: BetaFilters) =>
+ valueInList(row.industryName, filters.industries) &&
+ valueContains(row.industryName, filters.industrySearch) &&
+ (filters.rowType === undefined || row.rowType === filters.rowType) &&
+ numberAtLeast(row.beta, filters.minBeta) &&
+ numberAtMost(row.beta, filters.maxBeta) &&
+ numberAtLeast(row.unleveredBeta, filters.minUnleveredBeta) &&
+ numberAtMost(row.unleveredBeta, filters.maxUnleveredBeta) &&
+ numberAtLeast(row.numberOfFirms, filters.minNumberOfFirms) &&
+ numberAtMost(row.debtToEquityRatio, filters.maxDebtToEquityRatio);
+
+let withoutRaw = (row: T) => {
+ let { raw, ...rest } = row;
+ return rest;
+};
+
+let shapeRows = (rows: SternRow[], includeRaw: boolean) =>
+ includeRaw ? rows : rows.map(row => withoutRaw(row));
+
+export let applySourceFilters = (
+ sourceId: SourceId,
+ rows: SternRow[],
+ filters: SourceFilters
+): PaginatedRows => {
+ let filteredRows =
+ sourceId === 'erp'
+ ? rows.filter(row => isErpRow(row) && filterErpRow(row, filters as ErpFilters))
+ : rows.filter(row => isBetaRow(row) && filterBetaRow(row, filters as BetaFilters));
+
+ let returnedRows = filters.returnAll
+ ? filteredRows.slice(filters.offset)
+ : filteredRows.slice(filters.offset, filters.offset + filters.limit);
+
+ return {
+ filteredRows,
+ returnedRows: shapeRows(returnedRows, filters.includeRaw),
+ returnedRowCount: returnedRows.length,
+ filteredRowCount: filteredRows.length,
+ totalRowCount: rows.length,
+ truncated: filters.offset + returnedRows.length < filteredRows.length
+ };
+};
diff --git a/integrations/stern-financial-data/src/lib/sources.ts b/integrations/stern-financial-data/src/lib/sources.ts
new file mode 100644
index 0000000000..e9f80d9435
--- /dev/null
+++ b/integrations/stern-financial-data/src/lib/sources.ts
@@ -0,0 +1,148 @@
+export let PAGE_BASE = 'https://pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/';
+export let WORKBOOK_BASE = 'https://www.stern.nyu.edu/~adamodar/pc/datasets/';
+
+export let ERP_FIELDS = [
+ 'country',
+ 'moodysRating',
+ 'adjustedDefaultSpread',
+ 'countryRiskPremium',
+ 'equityRiskPremium',
+ 'corporateTaxRate',
+ 'sovereignCds',
+ 'erpBasedOnSovereignCds'
+] as const;
+
+export let BETA_FIELDS = [
+ 'industryName',
+ 'numberOfFirms',
+ 'beta',
+ 'debtToEquityRatio',
+ 'effectiveTaxRate',
+ 'unleveredBeta',
+ 'cashToFirmValue',
+ 'unleveredBetaCorrectedForCash',
+ 'hiloRisk',
+ 'standardDeviationOfEquity',
+ 'standardDeviationInOperatingIncomeLast10Years'
+] as const;
+
+export type ErpField = (typeof ERP_FIELDS)[number];
+export type BetaField = (typeof BETA_FIELDS)[number];
+export type SourceId = 'erp' | 'us_industry_betas' | 'global_industry_betas';
+export type SourceKind = 'erp' | 'beta';
+export type SourceType = 'workbook' | 'html';
+export type BetaRowType = 'industry' | 'aggregate';
+
+export type ErpRow = {
+ country: string;
+ moodysRating: string | null;
+ adjustedDefaultSpread: number | null;
+ countryRiskPremium: number | null;
+ equityRiskPremium: number | null;
+ corporateTaxRate: number | null;
+ sovereignCds: number | null;
+ erpBasedOnSovereignCds: number | null;
+ raw: Record;
+};
+
+export type BetaRow = {
+ industryName: string;
+ numberOfFirms: number | null;
+ beta: number | null;
+ debtToEquityRatio: number | null;
+ effectiveTaxRate: number | null;
+ unleveredBeta: number | null;
+ cashToFirmValue: number | null;
+ unleveredBetaCorrectedForCash: number | null;
+ hiloRisk: number | null;
+ standardDeviationOfEquity: number | null;
+ standardDeviationInOperatingIncomeLast10Years: number | null;
+ rowType: BetaRowType;
+ raw: Record;
+};
+
+export type SternRow = ErpRow | BetaRow;
+
+export type SternSource = {
+ id: SourceId;
+ title: string;
+ description: string;
+ kind: SourceKind;
+ pageUrl: string;
+ workbookUrl: string;
+ rowFields: readonly string[];
+ supportedFilters: readonly string[];
+};
+
+export let SOURCES: Record = {
+ erp: {
+ id: 'erp',
+ title: 'Country Equity Risk Premiums',
+ description:
+ "Country equity risk premiums, Moody's ratings, default spreads, tax rates, and sovereign CDS data.",
+ kind: 'erp',
+ pageUrl: `${PAGE_BASE}ctryprem.html`,
+ workbookUrl: `${WORKBOOK_BASE}ctryprem.xlsx`,
+ rowFields: ERP_FIELDS,
+ supportedFilters: [
+ 'countries',
+ 'countrySearch',
+ 'moodysRatings',
+ 'minEquityRiskPremium',
+ 'maxEquityRiskPremium',
+ 'minCountryRiskPremium',
+ 'maxCountryRiskPremium',
+ 'minCorporateTaxRate',
+ 'maxCorporateTaxRate',
+ 'hasSovereignCds'
+ ]
+ },
+ us_industry_betas: {
+ id: 'us_industry_betas',
+ title: 'US Industry Betas',
+ description:
+ 'US industry beta, leverage, tax, cash, risk, and volatility measures by industry.',
+ kind: 'beta',
+ pageUrl: `${PAGE_BASE}Betas.html`,
+ workbookUrl: `${WORKBOOK_BASE}betas.xls`,
+ rowFields: [...BETA_FIELDS, 'rowType'],
+ supportedFilters: [
+ 'industries',
+ 'industrySearch',
+ 'rowType',
+ 'minBeta',
+ 'maxBeta',
+ 'minUnleveredBeta',
+ 'maxUnleveredBeta',
+ 'minNumberOfFirms',
+ 'maxDebtToEquityRatio'
+ ]
+ },
+ global_industry_betas: {
+ id: 'global_industry_betas',
+ title: 'Global Industry Betas',
+ description:
+ 'Global industry beta, leverage, tax, cash, risk, and volatility measures by industry.',
+ kind: 'beta',
+ pageUrl: `${PAGE_BASE}BetasGlobal.html`,
+ workbookUrl: `${WORKBOOK_BASE}betaGlobal.xls`,
+ rowFields: [...BETA_FIELDS, 'rowType'],
+ supportedFilters: [
+ 'industries',
+ 'industrySearch',
+ 'rowType',
+ 'minBeta',
+ 'maxBeta',
+ 'minUnleveredBeta',
+ 'maxUnleveredBeta',
+ 'minNumberOfFirms',
+ 'maxDebtToEquityRatio'
+ ]
+ }
+};
+
+export let SOURCE_IDS = Object.keys(SOURCES) as SourceId[];
+export let SOURCE_LIST = SOURCE_IDS.map(sourceId => SOURCES[sourceId]);
+
+export let isSourceId = (value: string): value is SourceId =>
+ SOURCE_IDS.includes(value as SourceId);
diff --git a/integrations/stern-financial-data/src/spec.ts b/integrations/stern-financial-data/src/spec.ts
new file mode 100644
index 0000000000..97339da7b4
--- /dev/null
+++ b/integrations/stern-financial-data/src/spec.ts
@@ -0,0 +1,13 @@
+import { SlateSpecification } from 'slates';
+import { auth } from './auth';
+import { config } from './config';
+
+export let spec = SlateSpecification.create({
+ key: 'stern-financial-data',
+ name: 'Stern Financial Data',
+ description:
+ 'Query public NYU Stern financial datasets including country equity risk premiums and industry betas.',
+ metadata: {},
+ config,
+ auth
+});
diff --git a/integrations/stern-financial-data/src/tools/get-source.ts b/integrations/stern-financial-data/src/tools/get-source.ts
new file mode 100644
index 0000000000..38fbf9f2d8
--- /dev/null
+++ b/integrations/stern-financial-data/src/tools/get-source.ts
@@ -0,0 +1,220 @@
+import { SlateTool } from 'slates';
+import { z } from 'zod';
+import { SternFinancialDataClient } from '../lib/client';
+import { applySourceFilters, BetaFilters, ErpFilters } from '../lib/filters';
+import { sternFinancialDataServiceError } from '../lib/errors';
+import { spec } from '../spec';
+
+let commonControls = {
+ limit: z
+ .number()
+ .int()
+ .min(1)
+ .max(500)
+ .optional()
+ .default(25)
+ .describe('Maximum rows to return when returnAll is false. Defaults to 25.'),
+ offset: z
+ .number()
+ .int()
+ .min(0)
+ .optional()
+ .default(0)
+ .describe('Number of filtered rows to skip.'),
+ returnAll: z
+ .boolean()
+ .optional()
+ .default(false)
+ .describe('Return all filtered rows. Use sparingly because full Stern rows are wide.'),
+ includeRaw: z
+ .boolean()
+ .optional()
+ .default(false)
+ .describe('Include original cell text in each returned row.')
+};
+
+let percentageDescription = 'Decimal percentage value; for example, 0.05 means 5%.';
+
+let erpInputSchema = z.object({
+ source: z.literal('erp').describe('Country equity risk premium source.'),
+ ...commonControls,
+ countries: z
+ .array(z.string())
+ .optional()
+ .describe('Exact country names to include, case-insensitive.'),
+ countrySearch: z
+ .string()
+ .optional()
+ .describe('Case-insensitive substring search over country names.'),
+ moodysRatings: z
+ .array(z.string())
+ .optional()
+ .describe("Moody's ratings to include, such as Aaa, Baa2, Caa1, or NR."),
+ minEquityRiskPremium: z
+ .number()
+ .optional()
+ .describe(`Minimum equity risk premium. ${percentageDescription}`),
+ maxEquityRiskPremium: z
+ .number()
+ .optional()
+ .describe(`Maximum equity risk premium. ${percentageDescription}`),
+ minCountryRiskPremium: z
+ .number()
+ .optional()
+ .describe(`Minimum country risk premium. ${percentageDescription}`),
+ maxCountryRiskPremium: z
+ .number()
+ .optional()
+ .describe(`Maximum country risk premium. ${percentageDescription}`),
+ minCorporateTaxRate: z
+ .number()
+ .optional()
+ .describe(`Minimum corporate tax rate. ${percentageDescription}`),
+ maxCorporateTaxRate: z
+ .number()
+ .optional()
+ .describe(`Maximum corporate tax rate. ${percentageDescription}`),
+ hasSovereignCds: z
+ .boolean()
+ .optional()
+ .describe('Filter by whether the row has a sovereign CDS value.')
+});
+
+let betaFilterControls = {
+ industries: z
+ .array(z.string())
+ .optional()
+ .describe('Exact industry names to include, case-insensitive.'),
+ industrySearch: z
+ .string()
+ .optional()
+ .describe('Case-insensitive substring search over industry names.'),
+ rowType: z
+ .enum(['industry', 'aggregate'])
+ .optional()
+ .describe('Filter regular industries or total-market aggregate rows.'),
+ minBeta: z.number().optional().describe('Minimum levered beta.'),
+ maxBeta: z.number().optional().describe('Maximum levered beta.'),
+ minUnleveredBeta: z.number().optional().describe('Minimum unlevered beta.'),
+ maxUnleveredBeta: z.number().optional().describe('Maximum unlevered beta.'),
+ minNumberOfFirms: z
+ .number()
+ .int()
+ .min(0)
+ .optional()
+ .describe('Minimum number of firms in the industry row.'),
+ maxDebtToEquityRatio: z
+ .number()
+ .optional()
+ .describe(`Maximum debt-to-equity ratio. ${percentageDescription}`)
+};
+
+let usIndustryBetasInputSchema = z.object({
+ source: z.literal('us_industry_betas').describe('US industry beta source.'),
+ ...commonControls,
+ ...betaFilterControls
+});
+
+let globalIndustryBetasInputSchema = z.object({
+ source: z.literal('global_industry_betas').describe('Global industry beta source.'),
+ ...commonControls,
+ ...betaFilterControls
+});
+
+let inputSchema = z.discriminatedUnion('source', [
+ erpInputSchema,
+ usIndustryBetasInputSchema,
+ globalIndustryBetasInputSchema
+]);
+
+let outputSchema = z.object({
+ metadata: z.object({
+ source: z.string().describe('Source id that was retrieved.'),
+ title: z.string().describe('Source title.'),
+ pageUrl: z.string().describe('Stern HTML page URL.'),
+ workbookUrl: z.string().describe('Stern workbook URL.'),
+ retrievedAt: z.string().describe('ISO timestamp for this retrieval.'),
+ sourceType: z
+ .enum(['workbook', 'html'])
+ .describe('Whether rows came from the workbook or HTML fallback.'),
+ workbookFallbackReason: z
+ .string()
+ .optional()
+ .describe('Reason workbook extraction fell back to HTML, when applicable.')
+ }),
+ totalRowCount: z.number().describe('Rows extracted before filtering.'),
+ filteredRowCount: z.number().describe('Rows remaining after filters.'),
+ returnedRowCount: z.number().describe('Rows returned in this response.'),
+ offset: z.number().describe('Filtered-row offset applied.'),
+ limit: z.number().nullable().describe('Row limit applied, or null when returnAll is true.'),
+ returnAll: z.boolean().describe('Whether all filtered rows were requested.'),
+ truncated: z.boolean().describe('Whether additional filtered rows were omitted.'),
+ rows: z.array(z.any()).describe('Filtered Stern financial data rows.')
+});
+
+let validateRange = (min: number | undefined, max: number | undefined, label: string) => {
+ if (min !== undefined && max !== undefined && min > max) {
+ throw sternFinancialDataServiceError(`${label} minimum cannot be greater than maximum.`);
+ }
+};
+
+let validateInput = (input: z.infer) => {
+ if (input.source === 'erp') {
+ validateRange(input.minEquityRiskPremium, input.maxEquityRiskPremium, 'equityRiskPremium');
+ validateRange(
+ input.minCountryRiskPremium,
+ input.maxCountryRiskPremium,
+ 'countryRiskPremium'
+ );
+ validateRange(input.minCorporateTaxRate, input.maxCorporateTaxRate, 'corporateTaxRate');
+ return;
+ }
+
+ validateRange(input.minBeta, input.maxBeta, 'beta');
+ validateRange(input.minUnleveredBeta, input.maxUnleveredBeta, 'unleveredBeta');
+};
+
+export let getSource = SlateTool.create(spec, {
+ name: 'Get Source',
+ key: 'get_source',
+ description:
+ 'Retrieve a Stern financial data source and return filtered rows. Full output is available with returnAll=true, but use filters and limits when possible because full rows include many financial metrics and raw cell text can be large.',
+ instructions: [
+ 'Call list_sources first when you need the available source ids, row fields, or filter hints.',
+ 'Use filters such as countrySearch, industrySearch, rating, beta, premium, and rowType before requesting full output.',
+ 'Set includeRaw only when original formatted cell text is needed for audit or display.'
+ ],
+ tags: {
+ readOnly: true,
+ destructive: false
+ }
+})
+ .input(inputSchema)
+ .output(outputSchema)
+ .handleInvocation(async ctx => {
+ validateInput(ctx.input);
+
+ let client = new SternFinancialDataClient();
+ let result = await client.getSource(ctx.input.source);
+ let paginated = applySourceFilters(
+ ctx.input.source,
+ result.rows,
+ ctx.input.source === 'erp' ? (ctx.input as ErpFilters) : (ctx.input as BetaFilters)
+ );
+
+ return {
+ output: {
+ metadata: result.metadata,
+ totalRowCount: paginated.totalRowCount,
+ filteredRowCount: paginated.filteredRowCount,
+ returnedRowCount: paginated.returnedRowCount,
+ offset: ctx.input.offset,
+ limit: ctx.input.returnAll ? null : ctx.input.limit,
+ returnAll: ctx.input.returnAll,
+ truncated: paginated.truncated,
+ rows: paginated.returnedRows
+ },
+ message: `Retrieved **${paginated.returnedRowCount}** of **${paginated.filteredRowCount}** filtered Stern **${ctx.input.source}** rows.`
+ };
+ })
+ .build();
diff --git a/integrations/stern-financial-data/src/tools/index.ts b/integrations/stern-financial-data/src/tools/index.ts
new file mode 100644
index 0000000000..f91ede2bc4
--- /dev/null
+++ b/integrations/stern-financial-data/src/tools/index.ts
@@ -0,0 +1,2 @@
+export * from './get-source';
+export * from './list-sources';
diff --git a/integrations/stern-financial-data/src/tools/list-sources.ts b/integrations/stern-financial-data/src/tools/list-sources.ts
new file mode 100644
index 0000000000..511582689a
--- /dev/null
+++ b/integrations/stern-financial-data/src/tools/list-sources.ts
@@ -0,0 +1,50 @@
+import { SlateTool } from 'slates';
+import { z } from 'zod';
+import { SOURCE_LIST } from '../lib/sources';
+import { spec } from '../spec';
+
+export let listSources = SlateTool.create(spec, {
+ name: 'List Sources',
+ key: 'list_sources',
+ description:
+ 'List the NYU Stern financial data sources supported by this integration, including fields and filter hints.',
+ tags: {
+ readOnly: true,
+ destructive: false
+ }
+})
+ .input(z.object({}))
+ .output(
+ z.object({
+ sources: z
+ .array(
+ z.object({
+ id: z.string().describe('Source id to pass to get_source.'),
+ title: z.string().describe('Human-readable source title.'),
+ description: z.string().describe('Source contents.'),
+ pageUrl: z.string().describe('Stern HTML page URL.'),
+ workbookUrl: z.string().describe('Stern workbook URL.'),
+ rowFields: z.array(z.string()).describe('Fields returned for source rows.'),
+ supportedFilters: z
+ .array(z.string())
+ .describe('Filter inputs supported by get_source.')
+ })
+ )
+ .describe('Supported Stern financial data sources.')
+ })
+ )
+ .handleInvocation(async () => ({
+ output: {
+ sources: SOURCE_LIST.map(source => ({
+ id: source.id,
+ title: source.title,
+ description: source.description,
+ pageUrl: source.pageUrl,
+ workbookUrl: source.workbookUrl,
+ rowFields: [...source.rowFields],
+ supportedFilters: [...source.supportedFilters]
+ }))
+ },
+ message: `Found **${SOURCE_LIST.length}** Stern financial data sources.`
+ }))
+ .build();
diff --git a/integrations/stern-financial-data/src/types.d.ts b/integrations/stern-financial-data/src/types.d.ts
new file mode 100644
index 0000000000..22037b15b9
--- /dev/null
+++ b/integrations/stern-financial-data/src/types.d.ts
@@ -0,0 +1,3 @@
+declare module '@mixmark-io/domino' {
+ export function createWindow(html: string): any;
+}
diff --git a/integrations/stern-financial-data/tsconfig.json b/integrations/stern-financial-data/tsconfig.json
new file mode 100644
index 0000000000..2abe727831
--- /dev/null
+++ b/integrations/stern-financial-data/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "types": ["node"],
+ "lib": ["ESNext"],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+ "moduleResolution": "bundler",
+
+ "noEmit": true,
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ },
+ "include": ["src"]
+}
diff --git a/integrations/stern-financial-data/vitest.config.ts b/integrations/stern-financial-data/vitest.config.ts
new file mode 100644
index 0000000000..db5407c09a
--- /dev/null
+++ b/integrations/stern-financial-data/vitest.config.ts
@@ -0,0 +1,7 @@
+import { createSlatesVitestConfig } from '@slates/test/config';
+
+export default createSlatesVitestConfig({
+ test: {
+ include: ['src/**/*.test.ts', 'src/**/*.e2e.ts']
+ }
+});
diff --git a/packages/slates/example/google-sheets/auth.ts b/packages/slates/example/google-sheets/auth.ts
index 58801439ed..6d3060c07f 100644
--- a/packages/slates/example/google-sheets/auth.ts
+++ b/packages/slates/example/google-sheets/auth.ts
@@ -1,6 +1,22 @@
import { createAxios, SlateAuth } from 'slates';
import { z } from 'zod';
+type GoogleOauthInput = Record;
+
+type GoogleOauthOutput = {
+ token: string;
+ refreshToken?: string;
+ expiresAt?: string;
+};
+
+type GoogleOauthRefreshContext = {
+ output: GoogleOauthOutput;
+ input: GoogleOauthInput;
+ clientId: string;
+ clientSecret: string;
+ scopes: string[];
+};
+
let axios = createAxios({
baseURL: 'https://oauth2.googleapis.com'
});
@@ -85,7 +101,7 @@ export let auth = SlateAuth.create()
};
},
- handleTokenRefresh: async ctx => {
+ handleTokenRefresh: async (ctx: GoogleOauthRefreshContext) => {
if (!ctx.output.refreshToken) {
throw new Error('No refresh token available');
}