@distilled.cloud/stackit 0.0.0-stage → 1.0.0-rc.14

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.
Files changed (298) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +33 -2
  3. package/lib/credentials.d.ts +29 -0
  4. package/lib/credentials.d.ts.map +1 -0
  5. package/lib/credentials.js +49 -0
  6. package/lib/credentials.js.map +1 -0
  7. package/lib/errors.d.ts +36 -0
  8. package/lib/errors.d.ts.map +1 -0
  9. package/lib/errors.js +26 -0
  10. package/lib/errors.js.map +1 -0
  11. package/lib/index.d.ts +30 -0
  12. package/lib/index.d.ts.map +1 -0
  13. package/lib/index.js +30 -0
  14. package/lib/index.js.map +1 -0
  15. package/lib/pagination.d.ts +9 -0
  16. package/lib/pagination.d.ts.map +1 -0
  17. package/lib/pagination.js +45 -0
  18. package/lib/pagination.js.map +1 -0
  19. package/lib/protocol.d.ts +32 -0
  20. package/lib/protocol.d.ts.map +1 -0
  21. package/lib/protocol.js +147 -0
  22. package/lib/protocol.js.map +1 -0
  23. package/lib/protocol.test.d.ts +2 -0
  24. package/lib/protocol.test.d.ts.map +1 -0
  25. package/lib/protocol.test.js +181 -0
  26. package/lib/protocol.test.js.map +1 -0
  27. package/lib/retry.d.ts +31 -0
  28. package/lib/retry.d.ts.map +1 -0
  29. package/lib/retry.js +31 -0
  30. package/lib/retry.js.map +1 -0
  31. package/lib/services/alb.d.ts +603 -0
  32. package/lib/services/alb.d.ts.map +1 -0
  33. package/lib/services/alb.js +513 -0
  34. package/lib/services/alb.js.map +1 -0
  35. package/lib/services/alb_waf.d.ts +526 -0
  36. package/lib/services/alb_waf.d.ts.map +1 -0
  37. package/lib/services/alb_waf.js +521 -0
  38. package/lib/services/alb_waf.js.map +1 -0
  39. package/lib/services/authorization.d.ts +251 -0
  40. package/lib/services/authorization.d.ts.map +1 -0
  41. package/lib/services/authorization.js +323 -0
  42. package/lib/services/authorization.js.map +1 -0
  43. package/lib/services/automation.d.ts +321 -0
  44. package/lib/services/automation.d.ts.map +1 -0
  45. package/lib/services/automation.js +363 -0
  46. package/lib/services/automation.js.map +1 -0
  47. package/lib/services/cdn.d.ts +1239 -0
  48. package/lib/services/cdn.d.ts.map +1 -0
  49. package/lib/services/cdn.js +906 -0
  50. package/lib/services/cdn.js.map +1 -0
  51. package/lib/services/certificates.d.ts +201 -0
  52. package/lib/services/certificates.d.ts.map +1 -0
  53. package/lib/services/certificates.js +179 -0
  54. package/lib/services/certificates.js.map +1 -0
  55. package/lib/services/cost.d.ts +222 -0
  56. package/lib/services/cost.d.ts.map +1 -0
  57. package/lib/services/cost.js +177 -0
  58. package/lib/services/cost.js.map +1 -0
  59. package/lib/services/dns.d.ts +842 -0
  60. package/lib/services/dns.d.ts.map +1 -0
  61. package/lib/services/dns.js +726 -0
  62. package/lib/services/dns.js.map +1 -0
  63. package/lib/services/dremio.d.ts +358 -0
  64. package/lib/services/dremio.d.ts.map +1 -0
  65. package/lib/services/dremio.js +317 -0
  66. package/lib/services/dremio.js.map +1 -0
  67. package/lib/services/edge.d.ts +312 -0
  68. package/lib/services/edge.d.ts.map +1 -0
  69. package/lib/services/edge.js +357 -0
  70. package/lib/services/edge.js.map +1 -0
  71. package/lib/services/functions.d.ts +409 -0
  72. package/lib/services/functions.d.ts.map +1 -0
  73. package/lib/services/functions.js +399 -0
  74. package/lib/services/functions.js.map +1 -0
  75. package/lib/services/git.d.ts +497 -0
  76. package/lib/services/git.d.ts.map +1 -0
  77. package/lib/services/git.js +520 -0
  78. package/lib/services/git.js.map +1 -0
  79. package/lib/services/iaas.d.ts +2986 -0
  80. package/lib/services/iaas.d.ts.map +1 -0
  81. package/lib/services/iaas.js +3543 -0
  82. package/lib/services/iaas.js.map +1 -0
  83. package/lib/services/index.d.ts +51 -0
  84. package/lib/services/index.d.ts.map +1 -0
  85. package/lib/services/index.js +52 -0
  86. package/lib/services/index.js.map +1 -0
  87. package/lib/services/intake.d.ts +555 -0
  88. package/lib/services/intake.d.ts.map +1 -0
  89. package/lib/services/intake.js +475 -0
  90. package/lib/services/intake.js.map +1 -0
  91. package/lib/services/kms.d.ts +654 -0
  92. package/lib/services/kms.d.ts.map +1 -0
  93. package/lib/services/kms.js +663 -0
  94. package/lib/services/kms.js.map +1 -0
  95. package/lib/services/lb_ip_lists.d.ts +160 -0
  96. package/lib/services/lb_ip_lists.d.ts.map +1 -0
  97. package/lib/services/lb_ip_lists.js +154 -0
  98. package/lib/services/lb_ip_lists.js.map +1 -0
  99. package/lib/services/load_balancer.d.ts +563 -0
  100. package/lib/services/load_balancer.d.ts.map +1 -0
  101. package/lib/services/load_balancer.js +497 -0
  102. package/lib/services/load_balancer.js.map +1 -0
  103. package/lib/services/logme.d.ts +516 -0
  104. package/lib/services/logme.d.ts.map +1 -0
  105. package/lib/services/logme.js +574 -0
  106. package/lib/services/logme.js.map +1 -0
  107. package/lib/services/logs.d.ts +436 -0
  108. package/lib/services/logs.d.ts.map +1 -0
  109. package/lib/services/logs.js +475 -0
  110. package/lib/services/logs.js.map +1 -0
  111. package/lib/services/mariadb.d.ts +479 -0
  112. package/lib/services/mariadb.d.ts.map +1 -0
  113. package/lib/services/mariadb.js +551 -0
  114. package/lib/services/mariadb.js.map +1 -0
  115. package/lib/services/model_experiments.d.ts +306 -0
  116. package/lib/services/model_experiments.d.ts.map +1 -0
  117. package/lib/services/model_experiments.js +336 -0
  118. package/lib/services/model_experiments.js.map +1 -0
  119. package/lib/services/model_serving.d.ts +289 -0
  120. package/lib/services/model_serving.d.ts.map +1 -0
  121. package/lib/services/model_serving.js +287 -0
  122. package/lib/services/model_serving.js.map +1 -0
  123. package/lib/services/mongodb_flex.d.ts +826 -0
  124. package/lib/services/mongodb_flex.d.ts.map +1 -0
  125. package/lib/services/mongodb_flex.js +803 -0
  126. package/lib/services/mongodb_flex.js.map +1 -0
  127. package/lib/services/object_storage.d.ts +470 -0
  128. package/lib/services/object_storage.d.ts.map +1 -0
  129. package/lib/services/object_storage.js +540 -0
  130. package/lib/services/object_storage.js.map +1 -0
  131. package/lib/services/observability.d.ts +2750 -0
  132. package/lib/services/observability.d.ts.map +1 -0
  133. package/lib/services/observability.js +3158 -0
  134. package/lib/services/observability.js.map +1 -0
  135. package/lib/services/opensearch.d.ts +505 -0
  136. package/lib/services/opensearch.d.ts.map +1 -0
  137. package/lib/services/opensearch.js +563 -0
  138. package/lib/services/opensearch.js.map +1 -0
  139. package/lib/services/pim.d.ts +997 -0
  140. package/lib/services/pim.d.ts.map +1 -0
  141. package/lib/services/pim.js +736 -0
  142. package/lib/services/pim.js.map +1 -0
  143. package/lib/services/postgres_flex.d.ts +854 -0
  144. package/lib/services/postgres_flex.d.ts.map +1 -0
  145. package/lib/services/postgres_flex.js +853 -0
  146. package/lib/services/postgres_flex.js.map +1 -0
  147. package/lib/services/pubsub.d.ts +489 -0
  148. package/lib/services/pubsub.d.ts.map +1 -0
  149. package/lib/services/pubsub.js +519 -0
  150. package/lib/services/pubsub.js.map +1 -0
  151. package/lib/services/rabbitmq.d.ts +523 -0
  152. package/lib/services/rabbitmq.d.ts.map +1 -0
  153. package/lib/services/rabbitmq.js +574 -0
  154. package/lib/services/rabbitmq.js.map +1 -0
  155. package/lib/services/registry.d.ts +212 -0
  156. package/lib/services/registry.d.ts.map +1 -0
  157. package/lib/services/registry.js +198 -0
  158. package/lib/services/registry.js.map +1 -0
  159. package/lib/services/resource_manager.d.ts +458 -0
  160. package/lib/services/resource_manager.d.ts.map +1 -0
  161. package/lib/services/resource_manager.js +473 -0
  162. package/lib/services/resource_manager.js.map +1 -0
  163. package/lib/services/run_command.d.ts +175 -0
  164. package/lib/services/run_command.d.ts.map +1 -0
  165. package/lib/services/run_command.js +185 -0
  166. package/lib/services/run_command.js.map +1 -0
  167. package/lib/services/sca.d.ts +553 -0
  168. package/lib/services/sca.d.ts.map +1 -0
  169. package/lib/services/sca.js +445 -0
  170. package/lib/services/sca.js.map +1 -0
  171. package/lib/services/scf.d.ts +498 -0
  172. package/lib/services/scf.d.ts.map +1 -0
  173. package/lib/services/scf.js +670 -0
  174. package/lib/services/scf.js.map +1 -0
  175. package/lib/services/secrets_manager.d.ts +505 -0
  176. package/lib/services/secrets_manager.d.ts.map +1 -0
  177. package/lib/services/secrets_manager.js +613 -0
  178. package/lib/services/secrets_manager.js.map +1 -0
  179. package/lib/services/server_backup.d.ts +380 -0
  180. package/lib/services/server_backup.d.ts.map +1 -0
  181. package/lib/services/server_backup.js +454 -0
  182. package/lib/services/server_backup.js.map +1 -0
  183. package/lib/services/server_update.d.ts +292 -0
  184. package/lib/services/server_update.d.ts.map +1 -0
  185. package/lib/services/server_update.js +352 -0
  186. package/lib/services/server_update.js.map +1 -0
  187. package/lib/services/service_account.d.ts +465 -0
  188. package/lib/services/service_account.d.ts.map +1 -0
  189. package/lib/services/service_account.js +506 -0
  190. package/lib/services/service_account.js.map +1 -0
  191. package/lib/services/service_enablement.d.ts +149 -0
  192. package/lib/services/service_enablement.d.ts.map +1 -0
  193. package/lib/services/service_enablement.js +157 -0
  194. package/lib/services/service_enablement.js.map +1 -0
  195. package/lib/services/sfs.d.ts +721 -0
  196. package/lib/services/sfs.d.ts.map +1 -0
  197. package/lib/services/sfs.js +790 -0
  198. package/lib/services/sfs.js.map +1 -0
  199. package/lib/services/ske.d.ts +562 -0
  200. package/lib/services/ske.d.ts.map +1 -0
  201. package/lib/services/ske.js +558 -0
  202. package/lib/services/ske.js.map +1 -0
  203. package/lib/services/sqlserverflex.d.ts +956 -0
  204. package/lib/services/sqlserverflex.d.ts.map +1 -0
  205. package/lib/services/sqlserverflex.js +936 -0
  206. package/lib/services/sqlserverflex.js.map +1 -0
  207. package/lib/services/stackit_marketplace.d.ts +288 -0
  208. package/lib/services/stackit_marketplace.d.ts.map +1 -0
  209. package/lib/services/stackit_marketplace.js +379 -0
  210. package/lib/services/stackit_marketplace.js.map +1 -0
  211. package/lib/services/telemetry_link.d.ts +443 -0
  212. package/lib/services/telemetry_link.d.ts.map +1 -0
  213. package/lib/services/telemetry_link.js +532 -0
  214. package/lib/services/telemetry_link.js.map +1 -0
  215. package/lib/services/telemetry_router.d.ts +493 -0
  216. package/lib/services/telemetry_router.d.ts.map +1 -0
  217. package/lib/services/telemetry_router.js +484 -0
  218. package/lib/services/telemetry_router.js.map +1 -0
  219. package/lib/services/ufw.d.ts +295 -0
  220. package/lib/services/ufw.d.ts.map +1 -0
  221. package/lib/services/ufw.js +279 -0
  222. package/lib/services/ufw.js.map +1 -0
  223. package/lib/services/valkey.d.ts +505 -0
  224. package/lib/services/valkey.d.ts.map +1 -0
  225. package/lib/services/valkey.js +567 -0
  226. package/lib/services/valkey.js.map +1 -0
  227. package/lib/services/vpn.d.ts +1024 -0
  228. package/lib/services/vpn.d.ts.map +1 -0
  229. package/lib/services/vpn.js +931 -0
  230. package/lib/services/vpn.js.map +1 -0
  231. package/lib/services/workflows.d.ts +600 -0
  232. package/lib/services/workflows.d.ts.map +1 -0
  233. package/lib/services/workflows.js +508 -0
  234. package/lib/services/workflows.js.map +1 -0
  235. package/lib/traits.d.ts +24 -0
  236. package/lib/traits.d.ts.map +1 -0
  237. package/lib/traits.js +15 -0
  238. package/lib/traits.js.map +1 -0
  239. package/package.json +85 -3
  240. package/src/credentials.ts +84 -0
  241. package/src/errors.ts +50 -0
  242. package/src/index.ts +34 -0
  243. package/src/pagination.ts +57 -0
  244. package/src/protocol.test.ts +304 -0
  245. package/src/protocol.ts +225 -0
  246. package/src/retry.ts +47 -0
  247. package/src/services/alb.ts +1326 -0
  248. package/src/services/alb_waf.ts +1288 -0
  249. package/src/services/authorization.ts +704 -0
  250. package/src/services/automation.ts +796 -0
  251. package/src/services/cdn.ts +2576 -0
  252. package/src/services/certificates.ts +418 -0
  253. package/src/services/cost.ts +449 -0
  254. package/src/services/dns.ts +1936 -0
  255. package/src/services/dremio.ts +753 -0
  256. package/src/services/edge.ts +780 -0
  257. package/src/services/functions.ts +957 -0
  258. package/src/services/git.ts +1194 -0
  259. package/src/services/iaas.ts +7878 -0
  260. package/src/services/index.ts +51 -0
  261. package/src/services/intake.ts +1166 -0
  262. package/src/services/kms.ts +1537 -0
  263. package/src/services/lb_ip_lists.ts +342 -0
  264. package/src/services/load_balancer.ts +1266 -0
  265. package/src/services/logme.ts +1284 -0
  266. package/src/services/logs.ts +1049 -0
  267. package/src/services/mariadb.ts +1212 -0
  268. package/src/services/model_experiments.ts +741 -0
  269. package/src/services/model_serving.ts +654 -0
  270. package/src/services/mongodb_flex.ts +1932 -0
  271. package/src/services/object_storage.ts +1195 -0
  272. package/src/services/observability.ts +7364 -0
  273. package/src/services/opensearch.ts +1265 -0
  274. package/src/services/pim.ts +1985 -0
  275. package/src/services/postgres_flex.ts +2080 -0
  276. package/src/services/pubsub.ts +1193 -0
  277. package/src/services/rabbitmq.ts +1310 -0
  278. package/src/services/registry.ts +446 -0
  279. package/src/services/resource_manager.ts +1072 -0
  280. package/src/services/run_command.ts +413 -0
  281. package/src/services/sca.ts +1187 -0
  282. package/src/services/scf.ts +1396 -0
  283. package/src/services/secrets_manager.ts +1317 -0
  284. package/src/services/server_backup.ts +983 -0
  285. package/src/services/server_update.ts +738 -0
  286. package/src/services/service_account.ts +1121 -0
  287. package/src/services/service_enablement.ts +330 -0
  288. package/src/services/sfs.ts +1831 -0
  289. package/src/services/ske.ts +1386 -0
  290. package/src/services/sqlserverflex.ts +2307 -0
  291. package/src/services/stackit_marketplace.ts +787 -0
  292. package/src/services/telemetry_link.ts +1166 -0
  293. package/src/services/telemetry_router.ts +1110 -0
  294. package/src/services/ufw.ts +654 -0
  295. package/src/services/valkey.ts +1266 -0
  296. package/src/services/vpn.ts +2365 -0
  297. package/src/services/workflows.ts +1249 -0
  298. package/src/traits.ts +60 -0
package/package.json CHANGED
@@ -1,6 +1,88 @@
1
1
  {
2
2
  "name": "@distilled.cloud/stackit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.0-rc.14",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/alchemy-run/distilled",
8
+ "directory": "packages/stackit"
9
+ },
10
+ "files": [
11
+ "lib",
12
+ "src"
13
+ ],
14
+ "type": "module",
15
+ "sideEffects": false,
16
+ "module": "src/index.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./lib/index.d.ts",
20
+ "bun": "./src/index.ts",
21
+ "worker": "./src/index.ts",
22
+ "default": "./lib/index.js"
23
+ },
24
+ "./Credentials": {
25
+ "types": "./lib/credentials.d.ts",
26
+ "bun": "./src/credentials.ts",
27
+ "worker": "./src/credentials.ts",
28
+ "default": "./lib/credentials.js"
29
+ },
30
+ "./Errors": {
31
+ "types": "./lib/errors.d.ts",
32
+ "bun": "./src/errors.ts",
33
+ "worker": "./src/errors.ts",
34
+ "default": "./lib/errors.js"
35
+ },
36
+ "./Pagination": {
37
+ "types": "./lib/pagination.d.ts",
38
+ "bun": "./src/pagination.ts",
39
+ "worker": "./src/pagination.ts",
40
+ "default": "./lib/pagination.js"
41
+ },
42
+ "./Protocol": {
43
+ "types": "./lib/protocol.d.ts",
44
+ "bun": "./src/protocol.ts",
45
+ "worker": "./src/protocol.ts",
46
+ "default": "./lib/protocol.js"
47
+ },
48
+ "./Retry": {
49
+ "types": "./lib/retry.d.ts",
50
+ "bun": "./src/retry.ts",
51
+ "worker": "./src/retry.ts",
52
+ "default": "./lib/retry.js"
53
+ },
54
+ "./Traits": {
55
+ "types": "./lib/traits.d.ts",
56
+ "bun": "./src/traits.ts",
57
+ "worker": "./src/traits.ts",
58
+ "default": "./lib/traits.js"
59
+ },
60
+ "./*": {
61
+ "types": "./lib/services/*.d.ts",
62
+ "bun": "./src/services/*.ts",
63
+ "worker": "./src/services/*.ts",
64
+ "default": "./lib/services/*.js"
65
+ }
66
+ },
67
+ "dependencies": {
68
+ "@distilled.cloud/core": "1.0.0-rc.14"
69
+ },
70
+ "devDependencies": {
71
+ "@types/node": "26.2.0"
72
+ },
73
+ "peerDependencies": {
74
+ "effect": "^4.0.0"
75
+ },
76
+ "distilled": {
77
+ "generatedAt": "2026-10-06",
78
+ "specUpdatedAt": "2026-10-03"
79
+ },
80
+ "scripts": {
81
+ "build": "tsc -b",
82
+ "convert": "node --conditions=bun scripts/convert.ts",
83
+ "generate": "node --conditions=bun scripts/generate.ts",
84
+ "typecheck": "tsc -p tsconfig.json",
85
+ "specs:fetch": "git submodule update --force --init --depth=1 specs/spec-mirror-stackit",
86
+ "specs:update": "git submodule update --remote --force --depth=1 specs/spec-mirror-stackit"
87
+ }
6
88
  }
@@ -0,0 +1,84 @@
1
+ import { ConfigError } from "@distilled.cloud/core/errors";
2
+ /**
3
+ * STACKIT credentials — hand-written.
4
+ *
5
+ * The `Credentials` service resolves `{ token, region }` per request; the
6
+ * protocol layer formats the `Authorization: Bearer <token>` header from it
7
+ * and rewrites each operation's spec `servers` URL for the region. The token
8
+ * is a STACKIT service-account access token — the same value the official
9
+ * Go SDK, CLI and Terraform provider read from
10
+ * `STACKIT_SERVICE_ACCOUNT_TOKEN`. Key-file exchange
11
+ * (`STACKIT_SERVICE_ACCOUNT_KEY_PATH`) is not modelled: mint the token
12
+ * outside the SDK.
13
+ *
14
+ * Each STACKIT product has its own host (`iaas.api.eu01.stackit.cloud`,
15
+ * `ske.api.eu01.stackit.cloud`, …). The per-operation host is baked into
16
+ * the generated `T.Http` trait from the OpenAPI `servers` entry; `region`
17
+ * (default `eu01`) is substituted at request time. An explicit `apiBaseUrl`
18
+ * overrides that host for every call — a proxy, not a per-product map.
19
+ */
20
+ import * as EffectConfig from "effect/Config";
21
+ import * as Context from "effect/Context";
22
+ import * as Effect from "effect/Effect";
23
+ import * as Layer from "effect/Layer";
24
+ import * as Redacted from "effect/Redacted";
25
+
26
+ /** STACKIT's primary public region (Schwarz IT / Germany). */
27
+ export const DEFAULT_REGION = "eu01";
28
+
29
+ export interface Config {
30
+ readonly token: Redacted.Redacted<string>;
31
+ /**
32
+ * Region inserted into product hosts (`eu01` →
33
+ * `https://iaas.api.eu01.stackit.cloud`). `"global"` leaves the spec URL
34
+ * unchanged.
35
+ */
36
+ readonly region: string;
37
+ /** When set, every operation uses this host instead of the spec's. */
38
+ readonly apiBaseUrl?: string;
39
+ }
40
+
41
+ export class Credentials extends Context.Service<Credentials, Effect.Effect<Config>>()(
42
+ "StackitCredentials",
43
+ ) {}
44
+
45
+ const envConfig = EffectConfig.all({
46
+ // `STACKIT_SERVICE_ACCOUNT_TOKEN` / `STACKIT_REGION` are what the
47
+ // official Go SDK, CLI and Terraform provider read.
48
+ token: EffectConfig.String("STACKIT_SERVICE_ACCOUNT_TOKEN"),
49
+ region: EffectConfig.String("STACKIT_REGION").pipe(EffectConfig.withDefault(DEFAULT_REGION)),
50
+ apiBaseUrl: EffectConfig.String("STACKIT_API_BASE_URL").pipe(EffectConfig.option),
51
+ });
52
+
53
+ export const CredentialsFromEnv = Layer.succeed(
54
+ Credentials,
55
+ envConfig.pipe(
56
+ Effect.mapError(
57
+ () =>
58
+ new ConfigError({
59
+ message: "STACKIT_SERVICE_ACCOUNT_TOKEN environment variable is required",
60
+ }),
61
+ ),
62
+ Effect.map(({ token, region, apiBaseUrl }) => ({
63
+ token: Redacted.make(token),
64
+ region,
65
+ apiBaseUrl: apiBaseUrl._tag === "Some" ? apiBaseUrl.value : undefined,
66
+ })),
67
+ Effect.orDie,
68
+ ),
69
+ );
70
+
71
+ /** Convenience layer from a redacted token + optional region / base URL. */
72
+ export const credentials = (config: {
73
+ readonly token: Redacted.Redacted<string>;
74
+ readonly region?: string;
75
+ readonly apiBaseUrl?: string;
76
+ }): Layer.Layer<Credentials> =>
77
+ Layer.succeed(
78
+ Credentials,
79
+ Effect.succeed({
80
+ token: config.token,
81
+ region: config.region ?? DEFAULT_REGION,
82
+ apiBaseUrl: config.apiBaseUrl,
83
+ }),
84
+ );
package/src/errors.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * STACKIT-specific error types.
3
+ *
4
+ * Re-exports the common HTTP errors from core and adds the unknown-error
5
+ * and parse-error wrappers. STACKIT failures carry `{ message, error? }`
6
+ * (the `ErrorMessage` schema in most of the product specs); the protocol
7
+ * reads `message` for the human half and `error` as the machine-readable
8
+ * code on {@link UnknownStackitError}.
9
+ */
10
+ export {
11
+ BadGateway,
12
+ BadRequest,
13
+ Conflict,
14
+ ConfigError,
15
+ Forbidden,
16
+ GatewayTimeout,
17
+ InternalServerError,
18
+ Locked,
19
+ NotFound,
20
+ ServiceUnavailable,
21
+ TooManyRequests,
22
+ Unauthorized,
23
+ UnprocessableEntity,
24
+ HTTP_STATUS_MAP,
25
+ DEFAULT_ERRORS,
26
+ API_ERRORS,
27
+ } from "@distilled.cloud/core/errors";
28
+ export type { DefaultErrors } from "@distilled.cloud/core/errors";
29
+
30
+ import * as Category from "@distilled.cloud/core/category";
31
+ import * as Schema from "effect/Schema";
32
+
33
+ /** Unknown STACKIT error — returned when nothing else matches the failure. */
34
+ export class UnknownStackitError extends Schema.TaggedError<UnknownStackitError>()(
35
+ "UnknownStackitError",
36
+ {
37
+ code: Schema.optional(Schema.String),
38
+ message: Schema.optional(Schema.String),
39
+ body: Schema.Unknown,
40
+ },
41
+ ).pipe(Category.withServerError) {}
42
+
43
+ /** Schema parse error wrapper. */
44
+ export class StackitParseError extends Schema.TaggedError<StackitParseError>()(
45
+ "StackitParseError",
46
+ {
47
+ body: Schema.Unknown,
48
+ cause: Schema.Unknown,
49
+ },
50
+ ).pipe(Category.withParseError) {}
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @distilled.cloud/stackit — STACKIT Cloud API SDK for Effect.
3
+ *
4
+ * `./services` is generated by `scripts/generate.ts` from the Smithy models
5
+ * in `.generated-specs` (written by `scripts/convert.ts` from the OpenAPI
6
+ * documents mirrored at `specs/spec-mirror-stackit/specs/`). Everything else
7
+ * in this folder is hand-written.
8
+ *
9
+ * One module per STACKIT product (IaaS, SKE, DNS, Postgres Flex, …), using
10
+ * that product's latest published OpenAPI version. Auth is a service-account
11
+ * bearer token (`STACKIT_SERVICE_ACCOUNT_TOKEN`); each product's host is
12
+ * rewritten for `STACKIT_REGION` (default `eu01`).
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import * as Stackit from "@distilled.cloud/stackit";
17
+ *
18
+ * const { zones } = yield* Stackit.dns.listZones({
19
+ * projectId: "...",
20
+ * });
21
+ * ```
22
+ */
23
+ export * from "./credentials.ts";
24
+ export * from "./errors.ts";
25
+ export * as T from "./traits.ts";
26
+ export {
27
+ StackitProtocol,
28
+ applyRegion,
29
+ type StackitOpError,
30
+ type StackitOpContext,
31
+ } from "./protocol.ts";
32
+ export { paginateByTotalPages } from "./pagination.ts";
33
+ export * as Retry from "./retry.ts";
34
+ export * from "./services/index.ts";
@@ -0,0 +1,57 @@
1
+ import { getPath, type PaginationStrategy } from "@distilled.cloud/core/pagination";
2
+ /**
3
+ * STACKIT pagination — hand-written.
4
+ *
5
+ * Several STACKIT list operations page by NUMBER: `?page=N&pageSize=M` in,
6
+ * and a response carrying the collection beside `totalPages` (DNS zones /
7
+ * record sets, and the same envelope on other products). Core's
8
+ * `paginatePageNumber` treats `outputToken` as the NEXT page number, so
9
+ * pointing it at `totalPages` would jump to the last page; this strategy
10
+ * advances by one until `page >= totalPages`.
11
+ */
12
+ import * as Effect from "effect/Effect";
13
+ import * as Stream from "effect/Stream";
14
+
15
+ export type { PaginatedTrait, PaginationStrategy } from "@distilled.cloud/core/pagination";
16
+
17
+ /**
18
+ * Stream of pages using STACKIT's `page` / `totalPages` envelope: request
19
+ * `page`, `page+1`, … while `page < totalPages` and the collection is
20
+ * non-empty.
21
+ */
22
+ export const paginateByTotalPages: PaginationStrategy = (operation, input, pagination) => {
23
+ const inputToken = pagination.inputToken;
24
+ const totalPagesPath = pagination.outputToken;
25
+ const itemsPath = pagination.items;
26
+ if (!inputToken || !totalPagesPath) {
27
+ return Stream.die(
28
+ new Error("Total-pages pagination requires inputToken and outputToken (totalPages)"),
29
+ );
30
+ }
31
+
32
+ type State = { page: number; done: boolean };
33
+ const startPage = typeof input[inputToken] === "number" ? (input[inputToken] as number) : 1;
34
+
35
+ return Stream.unfold({ page: startPage, done: false } as State, (state) =>
36
+ Effect.gen(function* () {
37
+ if (state.done) return undefined;
38
+
39
+ const requestPayload = {
40
+ ...input,
41
+ [inputToken]: state.page,
42
+ } as typeof input;
43
+ const response = yield* operation(requestPayload);
44
+
45
+ const totalPages = getPath(response, totalPagesPath);
46
+ const items = itemsPath
47
+ ? (getPath(response, itemsPath) as readonly unknown[] | undefined)
48
+ : undefined;
49
+ const exhausted =
50
+ typeof totalPages !== "number" ||
51
+ state.page >= totalPages ||
52
+ (items !== undefined && items.length === 0);
53
+
54
+ return [response, { page: state.page + 1, done: exhausted } satisfies State] as const;
55
+ }),
56
+ );
57
+ };
@@ -0,0 +1,304 @@
1
+ import * as CoreErrors from "@distilled.cloud/core/errors";
2
+ import * as ResponseValidation from "@distilled.cloud/core/response-validation";
3
+ import * as Effect from "effect/Effect";
4
+ import * as HttpClient from "effect/http/HttpClient";
5
+ import * as HttpClientResponse from "effect/http/HttpClientResponse";
6
+ import * as Layer from "effect/Layer";
7
+ import * as Redacted from "effect/Redacted";
8
+ import { describe, expect, test } from "vitest";
9
+ import { Credentials, credentials } from "./credentials.ts";
10
+ import { StackitParseError, UnknownStackitError } from "./errors.ts";
11
+ import { applyRegion } from "./protocol.ts";
12
+ import * as Retry from "./retry.ts";
13
+ import { listServers } from "./services/iaas.ts";
14
+ import {
15
+ BadRequest,
16
+ Conflict,
17
+ deleteProject,
18
+ getProject,
19
+ listProjects,
20
+ NotFound,
21
+ } from "./services/resource_manager.ts";
22
+ import { getInstance } from "./services/secrets_manager.ts";
23
+
24
+ interface Captured {
25
+ readonly method: string;
26
+ readonly url: URL;
27
+ readonly headers: Record<string, string>;
28
+ readonly body: string | undefined;
29
+ }
30
+
31
+ const fakeStackit = (
32
+ status = 200,
33
+ body: string | null = "{}",
34
+ headers: Record<string, string> = {},
35
+ ) => {
36
+ const calls: Captured[] = [];
37
+ const layer = Layer.succeed(
38
+ HttpClient.HttpClient,
39
+ HttpClient.make((request, url) =>
40
+ Effect.sync(() => {
41
+ calls.push({
42
+ method: request.method,
43
+ url,
44
+ headers: { ...request.headers },
45
+ body:
46
+ request.body._tag === "Uint8Array"
47
+ ? new TextDecoder().decode(request.body.body)
48
+ : undefined,
49
+ });
50
+ return HttpClientResponse.fromWeb(request, new Response(body, { status, headers }));
51
+ }),
52
+ ),
53
+ );
54
+ return { calls, layer };
55
+ };
56
+
57
+ const creds = (config: { region?: string; apiBaseUrl?: string } = {}) =>
58
+ credentials({ token: Redacted.make("stackit-token"), ...config });
59
+
60
+ const run = <A, E>(
61
+ operation: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
62
+ http: Layer.Layer<HttpClient.HttpClient>,
63
+ credentialsLayer: Layer.Layer<Credentials> = creds(),
64
+ ) =>
65
+ Effect.runPromise(
66
+ operation.pipe(Retry.none, Effect.provide(credentialsLayer), Effect.provide(http)),
67
+ );
68
+
69
+ const runFlip = <A, E>(
70
+ operation: Effect.Effect<A, E, Credentials | HttpClient.HttpClient>,
71
+ http: Layer.Layer<HttpClient.HttpClient>,
72
+ ) =>
73
+ Effect.runPromise(
74
+ operation.pipe(Retry.none, Effect.provide(creds()), Effect.provide(http), Effect.flip),
75
+ );
76
+
77
+ const projectId = "11111111-2222-3333-4444-555555555555";
78
+
79
+ describe("applyRegion", () => {
80
+ test("inserts the region into a version-less product host", () => {
81
+ expect(applyRegion("https://ske.api.stackit.cloud", "eu01")).toBe(
82
+ "https://ske.api.eu01.stackit.cloud",
83
+ );
84
+ expect(applyRegion("https://load-balancer.api.stackit.cloud/", "eu02")).toBe(
85
+ "https://load-balancer.api.eu02.stackit.cloud/",
86
+ );
87
+ });
88
+
89
+ test("substitutes a glued {region} template, adding the trailing dot", () => {
90
+ expect(applyRegion("https://argus.api.{region}stackit.cloud", "eu01")).toBe(
91
+ "https://argus.api.eu01.stackit.cloud",
92
+ );
93
+ expect(applyRegion("https://argus.api.{region}stackit.cloud", "eu01.")).toBe(
94
+ "https://argus.api.eu01.stackit.cloud",
95
+ );
96
+ });
97
+
98
+ test("substitutes a non-glued {region} template verbatim", () => {
99
+ expect(applyRegion("https://example.{region}.stackit.cloud", "eu01")).toBe(
100
+ "https://example.eu01.stackit.cloud",
101
+ );
102
+ });
103
+
104
+ test("'global' leaves hosts alone and drops a leftover {region}", () => {
105
+ expect(applyRegion("https://dns.api.stackit.cloud", "global")).toBe(
106
+ "https://dns.api.stackit.cloud",
107
+ );
108
+ expect(applyRegion("https://argus.api.{region}stackit.cloud", "global")).toBe(
109
+ "https://argus.api.stackit.cloud",
110
+ );
111
+ });
112
+
113
+ test("leaves non-STACKIT and already-regional hosts alone", () => {
114
+ expect(applyRegion("https://proxy.example.com", "eu01")).toBe("https://proxy.example.com");
115
+ expect(applyRegion("https://ske.api.eu01.stackit.cloud", "eu02")).toBe(
116
+ "https://ske.api.eu01.stackit.cloud",
117
+ );
118
+ });
119
+ });
120
+
121
+ describe("request encoding", () => {
122
+ test("sends Bearer auth to the product host in the default region (eu01)", async () => {
123
+ const { calls, layer } = fakeStackit(200, JSON.stringify({}));
124
+ await run(getProject({ id: projectId }), layer);
125
+ expect(calls[0].headers.authorization).toBe("Bearer stackit-token");
126
+ expect(calls[0].url.href).toBe(
127
+ `https://resource-manager.api.eu01.stackit.cloud/v2/projects/${projectId}`,
128
+ );
129
+ });
130
+
131
+ test("each product uses its own host; the configured region is applied", async () => {
132
+ const iaas = fakeStackit(200, JSON.stringify({ items: [] }));
133
+ await run(
134
+ listServers({ projectId, region: "eu02", details: true }),
135
+ iaas.layer,
136
+ creds({ region: "eu02" }),
137
+ );
138
+ expect(iaas.calls[0].url.href).toBe(
139
+ `https://iaas.api.eu02.stackit.cloud/v2/projects/${projectId}/regions/eu02/servers?details=true`,
140
+ );
141
+
142
+ const secrets = fakeStackit(200, JSON.stringify({}));
143
+ await run(
144
+ getInstance({ projectId, instanceId: "i-1" }),
145
+ secrets.layer,
146
+ creds({ region: "eu02" }),
147
+ );
148
+ expect(secrets.calls[0].url.href).toBe(
149
+ `https://secrets-manager.api.eu02.stackit.cloud/v1/projects/${projectId}/instances/i-1`,
150
+ );
151
+ });
152
+
153
+ test("region 'global' keeps the spec host", async () => {
154
+ const { calls, layer } = fakeStackit(200, JSON.stringify({}));
155
+ await run(getProject({ id: projectId }), layer, creds({ region: "global" }));
156
+ expect(calls[0].url.origin).toBe("https://resource-manager.api.stackit.cloud");
157
+ });
158
+
159
+ test("an explicit apiBaseUrl overrides every product host", async () => {
160
+ const { calls, layer } = fakeStackit(200, JSON.stringify({}));
161
+ await run(
162
+ getInstance({ projectId, instanceId: "i-1" }),
163
+ layer,
164
+ creds({ apiBaseUrl: "http://localhost:8080" }),
165
+ );
166
+ expect(calls[0].url.href).toBe(`http://localhost:8080/v1/projects/${projectId}/instances/i-1`);
167
+ });
168
+
169
+ test("resolves credentials per request", async () => {
170
+ const { calls, layer } = fakeStackit(200, JSON.stringify({}));
171
+ let n = 0;
172
+ const rotating = Layer.succeed(
173
+ Credentials,
174
+ Effect.sync(() => ({ token: Redacted.make(`t${++n}`), region: n === 1 ? "eu01" : "eu02" })),
175
+ );
176
+ await run(getProject({ id: projectId }), layer, rotating);
177
+ await run(getProject({ id: projectId }), layer, rotating);
178
+ expect(calls.map((c) => [c.headers.authorization, c.url.host])).toEqual([
179
+ ["Bearer t1", "resource-manager.api.eu01.stackit.cloud"],
180
+ ["Bearer t2", "resource-manager.api.eu02.stackit.cloud"],
181
+ ]);
182
+ });
183
+
184
+ test("labels are encoded; array queries repeat; wire query names are used", async () => {
185
+ const get = fakeStackit(200, JSON.stringify({}));
186
+ await run(getProject({ id: "a b/c" }), get.layer);
187
+ expect(get.calls[0].url.pathname).toBe("/v2/projects/a%20b%2Fc");
188
+
189
+ const list = fakeStackit(200, JSON.stringify({ items: [] }));
190
+ await run(
191
+ listProjects({
192
+ containerIds: ["p1", "p2"],
193
+ limit: 10,
194
+ creation_time_start: "2024-01-01T00:00:00Z",
195
+ }),
196
+ list.layer,
197
+ );
198
+ const url = list.calls[0].url;
199
+ expect(url.searchParams.getAll("containerIds")).toEqual(["p1", "p2"]);
200
+ expect(url.searchParams.get("limit")).toBe("10");
201
+ expect(url.searchParams.get("creation-time-start")).toBe("2024-01-01T00:00:00Z");
202
+ });
203
+ });
204
+
205
+ describe("{ message, error } failure envelope", () => {
206
+ test("a declared status class carries `message`", async () => {
207
+ const { layer } = fakeStackit(
208
+ 404,
209
+ JSON.stringify({ message: "project not found", error: "Not Found" }),
210
+ );
211
+ const error = await runFlip(getProject({ id: projectId }), layer);
212
+ expect(error).toBeInstanceOf(NotFound);
213
+ expect(error).toMatchObject({ message: "project not found" });
214
+ });
215
+
216
+ test("409 matches the declared Conflict", async () => {
217
+ const { layer } = fakeStackit(
218
+ 409,
219
+ JSON.stringify({ message: "has children", error: "Conflict" }),
220
+ );
221
+ const error = await runFlip(deleteProject({ id: projectId }), layer);
222
+ expect(error).toBeInstanceOf(Conflict);
223
+ });
224
+
225
+ test("400 matches the declared BadRequest", async () => {
226
+ const { layer } = fakeStackit(
227
+ 400,
228
+ JSON.stringify({ message: "bad limit", error: "Bad Request" }),
229
+ );
230
+ const error = await runFlip(listProjects({ limit: -1 }), layer);
231
+ expect(error).toBeInstanceOf(BadRequest);
232
+ expect(error).toMatchObject({ message: "bad limit" });
233
+ });
234
+
235
+ test("undeclared statuses fall back to the core HTTP classes with Retry-After", async () => {
236
+ const { layer } = fakeStackit(429, JSON.stringify({ message: "slow down" }), {
237
+ "retry-after": "4",
238
+ });
239
+ const error = await runFlip(getProject({ id: projectId }), layer);
240
+ expect(error).toBeInstanceOf(CoreErrors.TooManyRequests);
241
+ expect(error).toMatchObject({ message: "slow down" });
242
+ expect((error as CoreErrors.TooManyRequests).retryAfter).toBeDefined();
243
+ });
244
+
245
+ test("unmapped 5xx becomes InternalServerError; plain text is the message", async () => {
246
+ const { layer } = fakeStackit(507, " insufficient storage\n");
247
+ const error = await runFlip(getProject({ id: projectId }), layer);
248
+ expect(error).toBeInstanceOf(CoreErrors.InternalServerError);
249
+ expect(error).toMatchObject({ message: "insufficient storage" });
250
+ });
251
+
252
+ test("an empty body uses `HTTP <status>` as message", async () => {
253
+ const { layer } = fakeStackit(404, "");
254
+ const error = await runFlip(getProject({ id: projectId }), layer);
255
+ expect(error).toBeInstanceOf(NotFound);
256
+ expect(error).toMatchObject({ message: "HTTP 404" });
257
+ });
258
+
259
+ test("unmapped 4xx becomes UnknownStackitError with `error` as code", async () => {
260
+ const body = { message: "teapot", error: "I'm a teapot" };
261
+ const { layer } = fakeStackit(418, JSON.stringify(body));
262
+ const error = await runFlip(getProject({ id: projectId }), layer);
263
+ expect(error).toBeInstanceOf(UnknownStackitError);
264
+ expect(error).toMatchObject({ code: "I'm a teapot", message: "teapot", body });
265
+ });
266
+
267
+ test("a numeric `code` is stringified when `error` is absent", async () => {
268
+ const body = { message: "teapot", code: 4180 };
269
+ const { layer } = fakeStackit(418, JSON.stringify(body));
270
+ const error = await runFlip(getProject({ id: projectId }), layer);
271
+ expect(error).toBeInstanceOf(UnknownStackitError);
272
+ expect(error).toMatchObject({ code: "4180", message: "teapot" });
273
+ });
274
+ });
275
+
276
+ describe("success decoding", () => {
277
+ test("the 2xx JSON body is the payload; empty bodies decode to {}", async () => {
278
+ const project = { containerId: "c", projectId, name: "p" };
279
+ const get = fakeStackit(200, JSON.stringify(project));
280
+ expect(await run(getProject({ id: projectId }), get.layer)).toEqual(project);
281
+
282
+ for (const [status, body] of [
283
+ [202, ""],
284
+ [204, null],
285
+ ] as const) {
286
+ const del = fakeStackit(status, body);
287
+ expect(await run(deleteProject({ id: projectId }), del.layer)).toEqual({});
288
+ }
289
+ });
290
+
291
+ test("strict validation fails a mismatched payload with StackitParseError", async () => {
292
+ const { layer } = fakeStackit(200, JSON.stringify({ name: 42 }));
293
+ const error = await Effect.runPromise(
294
+ getProject({ id: projectId }).pipe(
295
+ Retry.none,
296
+ Effect.provide(creds()),
297
+ Effect.provide(layer),
298
+ Effect.provide(ResponseValidation.strict),
299
+ Effect.flip,
300
+ ),
301
+ );
302
+ expect(error).toBeInstanceOf(StackitParseError);
303
+ });
304
+ });