Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/hyperdrive-planetscale-signature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"wrangler": minor
---

Add experimental `wrangler hyperdrive planetscale signature` command

Generates a signed authorization for creating a Cloudflare-billed PlanetScale database and prints it as JSON to stdout, so it can be piped into PlanetScale's own CLI, e.g.:

```sh
SIG=$(wrangler hyperdrive planetscale signature)
pscale database create <name> --org <org> \
--cloudflare-account-id "$(jq -r .account_id <<<"$SIG")" \
--cloudflare-timestamp "$(jq -r .timestamp <<<"$SIG")" \
--cloudflare-signature "$(jq -r .signature <<<"$SIG")"
```

This command is experimental and its interface may change.
112 changes: 112 additions & 0 deletions packages/wrangler/src/__tests__/hyperdrive.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe("hyperdrive help", () => {
wrangler hyperdrive delete <id> Delete a Hyperdrive config
wrangler hyperdrive get <id> Get a Hyperdrive config
wrangler hyperdrive list List Hyperdrive configs
wrangler hyperdrive planetscale Provision Cloudflare-billed PlanetScale databases [experimental]
wrangler hyperdrive update <id> Update a Hyperdrive config

GLOBAL FLAGS
Expand Down Expand Up @@ -75,6 +76,7 @@ describe("hyperdrive help", () => {
wrangler hyperdrive delete <id> Delete a Hyperdrive config
wrangler hyperdrive get <id> Get a Hyperdrive config
wrangler hyperdrive list List Hyperdrive configs
wrangler hyperdrive planetscale Provision Cloudflare-billed PlanetScale databases [experimental]
wrangler hyperdrive update <id> Update a Hyperdrive config

GLOBAL FLAGS
Expand All @@ -90,6 +92,116 @@ describe("hyperdrive help", () => {
});
});

describe("hyperdrive planetscale signature", () => {
mockAccountId();
mockApiToken();
runInTempDir();

const std = mockConsoleMethods();

function mockCreateDatabaseSignature(): Promise<{
accountId: string;
integration: string;
method: string;
}> {
return new Promise((resolve) => {
msw.use(
http.post(
"*/accounts/:accountId/hyperdrive/integrationsOperations/:integration/createDatabaseSignature",
async ({ params, request }) => {
resolve({
accountId: String(params.accountId),
integration: String(params.integration),
method: request.method,
});
return HttpResponse.json(
createFetchResult({
account_id: "some-account-id",
timestamp: "1700000000",
signature: "deadbeef",
})
);
}
)
);
});
}

it("should show the planetscale namespace help", async ({ expect }) => {
await runWrangler("hyperdrive planetscale");
await endEventLoop();

expect(std.err).toMatchInlineSnapshot(`""`);
expect(std.out).toMatchInlineSnapshot(`
"wrangler hyperdrive planetscale

Provision Cloudflare-billed PlanetScale databases [experimental]

COMMANDS
wrangler hyperdrive planetscale signature Generate a signed authorization for creating a Cloudflare-billed PlanetScale database [experimental]

GLOBAL FLAGS
-c, --config Path to Wrangler configuration file [string]
--cwd Run as if Wrangler was started in the specified directory instead of the current working directory [string]
-e, --env Environment to use for operations, and for selecting .env and .dev.vars files [string]
--env-file Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files [array]
-h, --help Show help [boolean]
--install-skills Install Cloudflare skills for detected AI coding agents before running the command [boolean] [default: false]
--profile Use a specific auth profile [string]
-v, --version Show version number [boolean]"
`);
});

it("should show the signature command help", async ({ expect }) => {
await runWrangler("hyperdrive planetscale signature --help");
await endEventLoop();

expect(std.err).toMatchInlineSnapshot(`""`);
expect(std.out).toMatchInlineSnapshot(`
"wrangler hyperdrive planetscale signature

Generate a signed authorization for creating a Cloudflare-billed PlanetScale database [experimental]

GLOBAL FLAGS
-c, --config Path to Wrangler configuration file [string]
--cwd Run as if Wrangler was started in the specified directory instead of the current working directory [string]
-e, --env Environment to use for operations, and for selecting .env and .dev.vars files [string]
--env-file Path to an .env file to load - can be specified multiple times - values from earlier files are overridden by values in later files [array]
-h, --help Show help [boolean]
--install-skills Install Cloudflare skills for detected AI coding agents before running the command [boolean] [default: false]
--profile Use a specific auth profile [string]
-v, --version Show version number [boolean]"
`);
});

it("should POST to the planetScale integration endpoint", async ({
expect,
}) => {
const reqProm = mockCreateDatabaseSignature();
await runWrangler("hyperdrive planetscale signature");

await expect(reqProm).resolves.toEqual({
accountId: "some-account-id",
integration: "planetScale",
method: "POST",
});
});

it("should print only the signature JSON, so it can be piped", async ({
expect,
}) => {
void mockCreateDatabaseSignature();
await runWrangler("hyperdrive planetscale signature");

expect(std.out).not.toContain("wrangler x.x.x");
expect(JSON.parse(std.out)).toEqual({
account_id: "some-account-id",
timestamp: "1700000000",
signature: "deadbeef",
});
});
});

describe("hyperdrive commands", () => {
mockAccountId();
mockApiToken();
Expand Down
22 changes: 22 additions & 0 deletions packages/wrangler/src/hyperdrive/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,25 @@ export async function patchConfig(
}
);
}

// Passed as flags to the partner's own CLI to authorize a Cloudflare-billed
// database creation.
export type CreateDatabaseSignature = {
account_id: string;
timestamp: string;
signature: string;
};

export async function createDatabaseSignature(
config: Config,
integration: string
): Promise<CreateDatabaseSignature> {
const accountId = await requireAuth(config);
return await fetchResult(
config,
`/accounts/${accountId}/hyperdrive/integrationsOperations/${integration}/createDatabaseSignature`,
{
method: "POST",
}
);
}
Comment on lines +190 to +202

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New Hyperdrive API call bypasses the required Cloudflare SDK

The new signing request talks to the Cloudflare API by hand-building a URL and calling the raw REST endpoint (fetchResult at packages/wrangler/src/hyperdrive/client.ts:195-201) instead of the official Cloudflare TypeScript SDK that the repository requires for all API access, so the call is untyped and can rely on undocumented endpoints.
Impact: Contributors reading the code get an unsupported pattern, and API changes will not be caught by types.

Repository rule and current code path

The root AGENTS.md anti-patterns list states: "Direct Cloudflare REST API calls → use the Cloudflare TypeScript SDK", and packages/wrangler/CONTRIBUTING.md ("Integration with Cloudflare REST API") repeats that the SDK, set up for every command handler, should be preferred. The new createDatabaseSignature() builds /accounts/${accountId}/hyperdrive/integrationsOperations/${integration}/createDatabaseSignature and posts via fetchResult. Note the rest of packages/wrangler/src/hyperdrive/client.ts predates the rule and uses the same pattern, so this may be an accepted deviation if the endpoint is not exposed by the SDK.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The underlying API endpoint hasn't been released yet. But when it is, we will update this code to use the generated Typescript SDK.

27 changes: 27 additions & 0 deletions packages/wrangler/src/hyperdrive/planetscale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createCommand, createNamespace } from "../core/create-command";
import { logger } from "../logger";
import { createDatabaseSignature } from "./client";

export const hyperdrivePlanetscaleNamespace = createNamespace({
metadata: {
description: "Provision Cloudflare-billed PlanetScale databases",
status: "experimental",
owner: "Product: Hyperdrive",
},
});

export const hyperdrivePlanetscaleSignatureCommand = createCommand({
metadata: {
description:
"Generate a signed authorization for creating a Cloudflare-billed PlanetScale database",
status: "experimental",
owner: "Product: Hyperdrive",
},
// Print only JSON, so the output can be piped into `pscale database create`.
behaviour: { printBanner: false },
args: {},
async handler(_args, { config }) {
const signature = await createDatabaseSignature(config, "planetScale");
logger.log(JSON.stringify(signature, null, 2));
},
});
12 changes: 12 additions & 0 deletions packages/wrangler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ import { hyperdriveDeleteCommand } from "./hyperdrive/delete";
import { hyperdriveGetCommand } from "./hyperdrive/get";
import { hyperdriveNamespace } from "./hyperdrive/index";
import { hyperdriveListCommand } from "./hyperdrive/list";
import {
hyperdrivePlanetscaleNamespace,
hyperdrivePlanetscaleSignatureCommand,
} from "./hyperdrive/planetscale";
import { hyperdriveUpdateCommand } from "./hyperdrive/update";
import { init } from "./init";
import {
Expand Down Expand Up @@ -1558,6 +1562,14 @@ export function createCLIParser(argv: string[]) {
},
{ command: "wrangler hyperdrive get", definition: hyperdriveGetCommand },
{ command: "wrangler hyperdrive list", definition: hyperdriveListCommand },
{
command: "wrangler hyperdrive planetscale",
definition: hyperdrivePlanetscaleNamespace,
},
{
command: "wrangler hyperdrive planetscale signature",
definition: hyperdrivePlanetscaleSignatureCommand,
},
{
command: "wrangler hyperdrive update",
definition: hyperdriveUpdateCommand,
Expand Down
Loading