Skip to content
Merged
4 changes: 2 additions & 2 deletions packages/aws/patches/s3.json
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,8 @@
"RequestLimitExceeded",
"SlowDown",
"NoSuchBucket",
"PermanentRedirect",
"NotFound"
"NoSuchKey",
"PermanentRedirect"
]
},
"putObject": {
Expand Down
4 changes: 2 additions & 2 deletions packages/aws/src/services/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14820,8 +14820,8 @@ export type DeleteObjectError =
| RequestLimitExceeded
| SlowDown
| NoSuchBucket
| NoSuchKey
| PermanentRedirect
| NotFound
| CommonErrors;
/**
* Removes an object from a bucket. The behavior depends on the bucket's versioning state:
Expand Down Expand Up @@ -14925,8 +14925,8 @@ export const deleteObject: API.OperationMethod<
RequestLimitExceeded,
SlowDown,
NoSuchBucket,
NoSuchKey,
PermanentRedirect,
NotFound,
],
protocol: AwsProtocol,
retry: Retry,
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,10 +366,19 @@ export const paginateRelay = <
const emptyPage =
pagination.items !== undefined &&
getItems(response, pagination.items).length === 0;
// A connection that keeps returning the same `endCursor` with
// `hasNextPage: true` (Railway `projects` has done this) would
// otherwise paginate forever.
const stuckCursor =
state.cursor !== undefined &&
nextCursor !== undefined &&
nextCursor !== null &&
nextCursor === state.cursor;

const nextState: State = {
cursor: nextCursor ?? undefined,
done: !hasNext || isTerminalToken(nextCursor) || emptyPage,
done:
!hasNext || isTerminalToken(nextCursor) || emptyPage || stuckCursor,
};

return [response, nextState] as const;
Expand Down
1 change: 1 addition & 0 deletions packages/fly-io/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export {
FlyApiProtocol,
FlyGraphqlProtocol,
SpritesProtocol,
MachineIdentity,
type FlyIoOpError,
type FlyIoOpContext,
type SpritesOpError,
Expand Down
52 changes: 41 additions & 11 deletions packages/fly-io/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
* The minted token is cached process-wide, then sent as
* `Authorization: Bearer` on sprite ops. There is no `SPRITES_TOKEN`.
*/
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Redacted from "effect/Redacted";
import * as Schema from "effect/Schema";
import type * as AST from "effect/SchemaAST";
Expand Down Expand Up @@ -127,17 +129,45 @@ const resolveFlyCreds = Effect.gen(function* () {
return yield* resolve;
});

export const FlyIoProtocol: Layer.Layer<API.Protocol> =
makeRestProtocol<Config>({
credentials: resolveFlyCreds,
baseUrl: (creds) => creds.apiBaseUrl,
headers: (creds) => ({
Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
}),
errorEnvelope: flyErrorEnvelope,
unknownError: ({ message, body }) =>
new UnknownFlyIoError({ message, body }),
});
/**
* Per-call signal that this Machines request is the Fly Machine itself
* (unix socket `/.fly/api`). Org tokens stay on {@link Credentials} for
* GetSecret in the same process; this service only changes headers:
* omit `Authorization`, send `Connection: close`.
*/
export class MachineIdentity extends Context.Service<MachineIdentity, true>()(
"FlyIoMachineIdentity",
) {}

const FlyIoProtocolRest: Layer.Layer<API.Protocol> = makeRestProtocol<Config>({
credentials: resolveFlyCreds,
baseUrl: (creds) => creds.apiBaseUrl,
headers: (creds) => ({
Authorization: `Bearer ${Redacted.value(creds.apiKey)}`,
}),
errorEnvelope: flyErrorEnvelope,
unknownError: ({ message, body }) => new UnknownFlyIoError({ message, body }),
});

export const FlyIoProtocol: Layer.Layer<API.Protocol> = Layer.effect(
API.Protocol,
Effect.gen(function* () {
const rest = yield* API.Protocol;
return API.Protocol.of({
encode: (args) =>
Effect.gen(function* () {
const request = yield* rest.encode(args);
const machine = yield* Effect.serviceOption(MachineIdentity);
if (Option.isNone(machine)) return request;
return request.pipe(
HttpClientRequest.removeHeader("authorization"),
HttpClientRequest.setHeader("connection", "close"),
);
}),
decode: rest.decode,
});
}),
).pipe(Layer.provide(FlyIoProtocolRest));

/** MPG UI-EX REST at `https://api.fly.io` with the Machines bearer token. */
export const FlyApiProtocol: Layer.Layer<API.Protocol> =
Expand Down
56 changes: 55 additions & 1 deletion packages/hetzner/.generated-specs/servers.json
Original file line number Diff line number Diff line change
Expand Up @@ -3826,7 +3826,15 @@
"code": 200
},
"smithy.api#documentation": "Create a Server\n\nCreates a new Server. Returns preliminary information about the Server as well as an Action that covers progress of creation.\n\n#### Operation specific errors"
}
},
"errors": [
{
"target": "com.hetzner.servers#ServerLimitExceeded"
},
{
"target": "com.hetzner.servers#ServerPlacementError"
}
]
},
"com.hetzner.servers#GetServerRequest": {
"type": "structure",
Expand Down Expand Up @@ -7300,6 +7308,52 @@
"smithy.api#title": "Hetzner Cloud API",
"smithy.api#documentation": "# Overview\n\nThis is the official documentation for the Hetzner Cloud API.\n\n## Introduction\n\nThe Hetzner Cloud API operates over HTTPS and uses JSON as its data format. The API is a RESTful API and utilizes HTTP methods and HTTP status codes to specify requests and responses.\n\nAs an alternative to working directly with our API you may also consider to use:\n\n- Our CLI program [hcloud](https://github.com/hetznercloud/cli)\n- Our [library for Go](https://github.com/hetznercloud/hcloud-go)\n- Our [library for Python](https://github.com/hetznercloud/hcloud-python)\n\nYou can find even more libraries, tools and integrations on our [Awesome List on GitHub](https://github.com/hetznercloud/awesome-hcloud).\n\n### Open source credits\n\nIf you are developing an open-source project that supports or intends to add support for Hetzner APIs, you may be eligible for a free one-time credit of up to € 50 / $ 50 on your account. Please contact us via the support page on your [Hetzner Console](https://console.hetzner.cloud/support) and let us know the following:\n\n- The name of the project you are working on\n- A short description of the project\n- Link to the project website or repo where the project is hosted\n- Affiliation with / role in the project (e.g. project maintainer)\n- Link to some other open-source work you have already done (if you have done so)\n\n**Note:** We only consider rewards for projects that provide Hetzner-specific functionality or integrations. For example, our Object Storage exposes a standard S3 API without any Hetzner-specific extensions. Projects that focus solely on generic S3 capabilities (e.g., general S3 clients or SDKs) are not Hetzner-specific and are therefore not eligible for Hetzner Rewards.\n\n## Getting Started\n\nTo get started using the API you first need an API token. Sign in into the [Hetzner Console](https://console.hetzner.com/) choose a Project, go to `Security` → `API Tokens`, and generate a new token. Make sure to copy the token because it won’t be shown to you again. A token is bound to a Project, to interact with the API of another Project you have to create a new token inside the Project. Let’s say your new token is `LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj`.\n\nYou’re now ready to do your first request against the API. To get a list of all Servers in your Project, issue the example request on the right side using [curl](https://curl.se/).\n\nMake sure to replace the token in the example command with the token you have just created. Since your Project probably does not contain any Servers yet, the example response will look like the response on the right side. We will almost always provide a resource root like `servers` inside the example response. A response can also contain a `meta` object with information like [Pagination](#description/pagination).\n\n**Example Request**\n\n```shell\ncurl -H \"Authorization: Bearer LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\" \\\n https://api.hetzner.cloud/v1/servers\n```\n\n**Example Response**\n\n```json\n{\n \"servers\": [],\n \"meta\": {\n \"pagination\": {\n \"page\": 1,\n \"per_page\": 25,\n \"previous_page\": null,\n \"next_page\": null,\n \"last_page\": 1,\n \"total_entries\": 0\n }\n }\n}\n```\n\n## Authentication\n\nAll requests to the Hetzner Cloud API must be authenticated via a API token. Include your secret API token in every request you send to the API with the `Authorization` HTTP header.\n\nTo create a new API token for your Project, switch into the [Hetzner Console](https://console.hetzner.com/) choose a Project, go to `Security` → `API Tokens`, and generate a new token.\n\n**Example Authorization header**\n\n```http\nAuthorization: Bearer LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj\n```\n\n## Query Parameters\n\nThe API makes use of query parameters to sort and filter responses. The parameter names and values must be URI encoded according to [RFC2396](https://datatracker.ietf.org/doc/html/rfc2396). Query parameters of type `array` can be used multiple times:\n\n**Example query parameters for pagination**\n\n```\nhttps://api.hetzner.cloud/v1/certificates?page=1&page_size=12\n```\n\n**Example use of multiple values for a parameter**\n\n```\nhttps://api.hetzner.cloud/v1/certificates?type=uploaded&type=managed\n```\n\n**Example use of an encoded parameter**\n\n```\nhttps://api.hetzner.cloud/v1/certificates?label_selector=key%3Dvalue\n```\n\n## Errors\n\nErrors are indicated by HTTP status codes. Further, the response of the request which generated the error contains an error code, an error message, and, optionally, error details. The schema of the error details object depends on the error code.\n\nThe error response contains the following keys:\n\n\n**Example response**\n\n```json\n{\n \"error\": {\n \"code\": \"invalid_input\",\n \"message\": \"invalid input in field 'broken_field': is too long\",\n \"details\": {\n \"fields\": [\n {\n \"name\": \"broken_field\",\n \"messages\": [\"is too long\"]\n }\n ]\n }\n }\n}\n```\n\n### Error Codes\n\n\n**invalid_input**\n\n```json\n{\n \"error\": {\n \"code\": \"invalid_input\",\n \"message\": \"invalid input in field 'broken_field': is too long\",\n \"details\": {\n \"fields\": [\n {\n \"name\": \"broken_field\",\n \"messages\": [\"is too long\"]\n }\n ]\n }\n }\n}\n```\n\n**uniqueness_error**\n\n```json\n{\n \"error\": {\n \"code\": \"uniqueness_error\",\n \"message\": \"SSH key with the same fingerprint already exists\",\n \"details\": {\n \"fields\": [\n {\n \"name\": \"public_key\"\n }\n ]\n }\n }\n}\n```\n\n**resource_limit_exceeded**\n\n```json\n{\n \"error\": {\n \"code\": \"resource_limit_exceeded\",\n \"message\": \"project limit exceeded\",\n \"details\": {\n \"limits\": [\n {\n \"name\": \"project_limit\"\n }\n ]\n }\n }\n}\n```\n\n**deprecated_api_endpoint**\n\n```json\n{\n \"error\": {\n \"code\": \"deprecated_api_endpoint\",\n \"message\": \"API functionality was removed\",\n \"details\": {\n \"announcement\": \"https://docs.hetzner.cloud/changelog#2023-07-20-foo-endpoint-is-deprecated\"\n }\n }\n}\n```\n\n## Actions\n\nActions represent asynchronous tasks within the API, targeting one or more resources. Triggering changes in the API may return a `running` action.\n\nAn action should be waited upon, until it reaches either the `success` or `error` state. Avoid polling the action's state too frequently to reduce the risk of exhausting your API requests and hitting the [rate limit](#description/rate-limiting).\n\nIf an action fails, it will contain details about the underlying error.\n\nOnce the asynchronous tasks have completed and the targeted resources are in a consistent state, the action is marked as succeeded.\n\nIn some cases, you may trigger multiple changes at once, and only wait for the returned actions at a later stage.\n\n## Labels\n\nLabels are `key/value` pairs that can be attached to all resources.\n\nValid label keys have two segments: an optional prefix and name, separated by a slash (`/`). The name segment is required and must be a string of 63 characters or less, beginning and ending with an alphanumeric character (`[a-z0-9A-Z]`) with dashes (`-`), underscores (`_`), dots (`.`), and alphanumerics between. The prefix is optional. If specified, the prefix must be a DNS subdomain: a series of DNS labels separated by dots (`.`), not longer than 253 characters in total, followed by a slash (`/`).\n\nValid label values must be a string of 63 characters or less and must be empty or begin and end with an alphanumeric character (`[a-z0-9A-Z]`) with dashes (`-`), underscores (`_`), dots (`.`), and alphanumerics between.\n\nThe `hetzner.cloud/` prefix is reserved and cannot be used.\n\n**Example Labels**\n\n```json\n{\n \"labels\": {\n \"environment\": \"development\",\n \"service\": \"backend\",\n \"example.com/my\": \"label\",\n \"just-a-key\": \"\"\n }\n}\n```\n\n## Label Selector\n\nFor resources with labels, you can filter resources by their labels using the label selector query language.\n\n\n### Examples\n\n- Returns all resources that have a `env=production` label and that don't have a `type=database` label:\n\n `env=production,type!=database`\n\n- Returns all resources that have a `env=testing` or `env=staging` label:\n\n `env in (testing,staging)`\n\n- Returns all resources that don't have a `type` label:\n\n `!type`\n\n## Pagination\n\nResponses which return multiple items support pagination. If they do support pagination, it can be controlled with following query string parameters:\n\n- A `page` parameter specifies the page to fetch. The number of the first page is 1.\n- A `per_page` parameter specifies the number of items returned per page. The default value is 25, the maximum value is 50 except otherwise specified in the documentation.\n\nResponses contain a `Link` header with pagination information.\n\nAdditionally, if the response body is JSON and the root object is an object, that object has a `pagination` object inside the `meta` object with pagination information:\n\n**Example Pagination**\n\n```json\n{\n \"servers\": [...],\n \"meta\": {\n \"pagination\": {\n \"page\": 2,\n \"per_page\": 25,\n \"previous_page\": 1,\n \"next_page\": 3,\n \"last_page\": 4,\n \"total_entries\": 100\n }\n }\n}\n```\n\nThe keys `previous_page`, `next_page`, `last_page`, and `total_entries` may be `null` when on the first page, last page, or when the total number of entries is unknown.\n\n**Example Pagination Link header**\n\n```http\nLink: <https://api.hetzner.cloud/v1/actions?page=2&per_page=5>; rel=\"prev\",\n <https://api.hetzner.cloud/v1/actions?page=4&per_page=5>; rel=\"next\",\n <https://api.hetzner.cloud/v1/actions?page=6&per_page=5>; rel=\"last\"\n```\n\nLine breaks have been added for display purposes only and responses may only contain some of the above `rel` values.\n\n## Rate Limiting\n\nAll requests, whether they are authenticated or not, are subject to rate limiting. If you have reached your limit, your requests will be handled with a `429 Too Many Requests` error. Burst requests are allowed. Responses contain several headers which provide information about your current rate limit status.\n\n- The `RateLimit-Limit` header contains the total number of requests you can perform per hour.\n- The `RateLimit-Remaining` header contains the number of requests remaining in the current rate limit time frame.\n- The `RateLimit-Reset` header contains a UNIX timestamp of the point in time when your rate limit will have recovered, and you will have the full number of requests available again.\n\nThe default limit is 3600 requests per hour and per Project. The number of remaining requests increases gradually. For example, when your limit is 3600 requests per hour, the number of remaining requests will increase by 1 every second.\n\n## Server Metadata\n\nYour Server can discover metadata about itself by doing a HTTP request to specific URLs. The following data is available:\n\n\n**Example: Summary**\n\n```shell\n$ curl http://169.254.169.254/hetzner/v1/metadata\n```\n\n```yaml\navailability-zone: hel1-dc2\nhostname: my-server\ninstance-id: 42\npublic-ipv4: 1.2.3.4\nregion: eu-central\n```\n\n**Example: Hostname**\n\n```shell\n$ curl http://169.254.169.254/hetzner/v1/metadata/hostname\nmy-server\n```\n\n**Example: Instance ID**\n\n```shell\n$ curl http://169.254.169.254/hetzner/v1/metadata/instance-id\n42\n```\n\n**Example: Public IPv4**\n\n```shell\n$ curl http://169.254.169.254/hetzner/v1/metadata/public-ipv4\n1.2.3.4\n```\n\n**Example: Private Networks**\n\n```shell\n$ curl http://169.254.169.254/hetzner/v1/metadata/private-networks\n```\n\n```yaml\n- ip: 10.0.0.2\n alias_ips: [10.0.0.3, 10.0.0.4]\n interface_num: 1\n mac_address: 86:00:00:2a:7d:e0\n network_id: 1234\n network_name: nw-test1\n network: 10.0.0.0/8\n subnet: 10.0.0.0/24\n gateway: 10.0.0.1\n- ip: 192.168.0.2\n alias_ips: []\n interface_num: 2\n mac_address: 86:00:00:2a:7d:e1\n network_id: 4321\n network_name: nw-test2\n network: 192.168.0.0/16\n subnet: 192.168.0.0/24\n gateway: 192.168.0.1\n```\n\n**Example: Availability Zone**\n\n```shell\n$ curl http://169.254.169.254/hetzner/v1/metadata/availability-zone\nhel1-dc2\n```\n\n**Example: Region**\n\n```shell\n$ curl http://169.254.169.254/hetzner/v1/metadata/region\neu-central\n```\n\n## Sorting\n\nSome responses which return multiple items support sorting. If they do support sorting the documentation states which fields can be used for sorting. You specify sorting with the `sort` query string parameter. You can sort by multiple fields. You can set the sort direction by appending `:asc` or `:desc` to the field name. By default, ascending sorting is used.\n\n**Example: Sorting**\n\n```\nhttps://api.hetzner.cloud/v1/actions?sort=status\nhttps://api.hetzner.cloud/v1/actions?sort=status:asc\nhttps://api.hetzner.cloud/v1/actions?sort=status:desc\nhttps://api.hetzner.cloud/v1/actions?sort=status:asc&sort=command:desc\n```\n\n## Deprecation Notices\n\nYou can find all announced deprecations in our [Changelog](/changelog)."
}
},
"com.hetzner.servers#ServerLimitExceeded": {
"type": "structure",
"members": {
"code": {
"target": "smithy.api#Integer"
},
"message": {
"target": "smithy.api#String"
}
},
"traits": {
"smithy.api#error": "client",
"smithy.api#documentation": "The project has no remaining Server quota (Hetzner `resource_limit_exceeded`, HTTP 403). Often a race after a delete — retry.",
"com.distilled.openapi#errorMatchers": [
{
"status": 403,
"message": {
"includes": "server limit"
}
}
]
}
},
"com.hetzner.servers#ServerPlacementError": {
"type": "structure",
"members": {
"code": {
"target": "smithy.api#Integer"
},
"message": {
"target": "smithy.api#String"
}
},
"traits": {
"smithy.api#error": "client",
"smithy.api#documentation": "The Server could not be placed (Hetzner `resource_unavailable` / `placement_error`, HTTP 412). Spread groups and location capacity recover — retry.",
"com.distilled.openapi#errorMatchers": [
{
"status": 412,
"message": {
"includes": "placement"
}
}
]
}
}
}
}
Loading
Loading