@alchemy.run/planetscale 0.0.0 → 2.0.0-beta.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +83 -4
- package/lib/AuthProvider.d.ts +64 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +395 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/Branch.d.ts +128 -0
- package/lib/Branch.d.ts.map +1 -0
- package/lib/Branch.js +420 -0
- package/lib/Branch.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/Credentials.d.ts +33 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +59 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Database.d.ts +138 -0
- package/lib/Database.d.ts.map +1 -0
- package/lib/Database.js +1 -0
- package/lib/Database.js.map +1 -0
- package/lib/MySQL/MySQLBranch.d.ts +87 -0
- package/lib/MySQL/MySQLBranch.d.ts.map +1 -0
- package/lib/MySQL/MySQLBranch.js +15 -0
- package/lib/MySQL/MySQLBranch.js.map +1 -0
- package/lib/MySQL/MySQLClusterSize.d.ts +32 -0
- package/lib/MySQL/MySQLClusterSize.d.ts.map +1 -0
- package/lib/MySQL/MySQLClusterSize.js +111 -0
- package/lib/MySQL/MySQLClusterSize.js.map +1 -0
- package/lib/MySQL/MySQLDatabase.d.ts +141 -0
- package/lib/MySQL/MySQLDatabase.d.ts.map +1 -0
- package/lib/MySQL/MySQLDatabase.js +294 -0
- package/lib/MySQL/MySQLDatabase.js.map +1 -0
- package/lib/MySQL/MySQLMigrations.d.ts +20 -0
- package/lib/MySQL/MySQLMigrations.d.ts.map +1 -0
- package/lib/MySQL/MySQLMigrations.js +72 -0
- package/lib/MySQL/MySQLMigrations.js.map +1 -0
- package/lib/MySQL/MySQLOrigin.d.ts +15 -0
- package/lib/MySQL/MySQLOrigin.d.ts.map +1 -0
- package/lib/MySQL/MySQLOrigin.js +2 -0
- package/lib/MySQL/MySQLOrigin.js.map +1 -0
- package/lib/MySQL/MySQLPassword.d.ts +116 -0
- package/lib/MySQL/MySQLPassword.d.ts.map +1 -0
- package/lib/MySQL/MySQLPassword.js +212 -0
- package/lib/MySQL/MySQLPassword.js.map +1 -0
- package/lib/MySQL/index.d.ts +6 -0
- package/lib/MySQL/index.d.ts.map +1 -0
- package/lib/MySQL/index.js +6 -0
- package/lib/MySQL/index.js.map +1 -0
- package/lib/OAuthClient.d.ts +32 -0
- package/lib/OAuthClient.d.ts.map +1 -0
- package/lib/OAuthClient.js +52 -0
- package/lib/OAuthClient.js.map +1 -0
- package/lib/Postgres/PostgresBranch.d.ts +75 -0
- package/lib/Postgres/PostgresBranch.d.ts.map +1 -0
- package/lib/Postgres/PostgresBranch.js +15 -0
- package/lib/Postgres/PostgresBranch.js.map +1 -0
- package/lib/Postgres/PostgresClusterSize.d.ts +77 -0
- package/lib/Postgres/PostgresClusterSize.d.ts.map +1 -0
- package/lib/Postgres/PostgresClusterSize.js +89 -0
- package/lib/Postgres/PostgresClusterSize.js.map +1 -0
- package/lib/Postgres/PostgresDatabase.d.ts +95 -0
- package/lib/Postgres/PostgresDatabase.d.ts.map +1 -0
- package/lib/Postgres/PostgresDatabase.js +294 -0
- package/lib/Postgres/PostgresDatabase.js.map +1 -0
- package/lib/Postgres/PostgresDefaultRole.d.ts +94 -0
- package/lib/Postgres/PostgresDefaultRole.d.ts.map +1 -0
- package/lib/Postgres/PostgresDefaultRole.js +187 -0
- package/lib/Postgres/PostgresDefaultRole.js.map +1 -0
- package/lib/Postgres/PostgresMigrations.d.ts +16 -0
- package/lib/Postgres/PostgresMigrations.d.ts.map +1 -0
- package/lib/Postgres/PostgresMigrations.js +65 -0
- package/lib/Postgres/PostgresMigrations.js.map +1 -0
- package/lib/Postgres/PostgresOrigin.d.ts +15 -0
- package/lib/Postgres/PostgresOrigin.d.ts.map +1 -0
- package/lib/Postgres/PostgresOrigin.js +2 -0
- package/lib/Postgres/PostgresOrigin.js.map +1 -0
- package/lib/Postgres/PostgresRole.d.ts +142 -0
- package/lib/Postgres/PostgresRole.d.ts.map +1 -0
- package/lib/Postgres/PostgresRole.js +268 -0
- package/lib/Postgres/PostgresRole.js.map +1 -0
- package/lib/Postgres/index.d.ts +7 -0
- package/lib/Postgres/index.d.ts.map +1 -0
- package/lib/Postgres/index.js +7 -0
- package/lib/Postgres/index.js.map +1 -0
- package/lib/Providers.d.ts +24 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +40 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Util.d.ts +93 -0
- package/lib/Util.d.ts.map +1 -0
- package/lib/Util.js +135 -0
- package/lib/Util.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +9 -0
- package/lib/index.js.map +1 -0
- package/package.json +79 -3
- package/src/AuthProvider.ts +584 -0
- package/src/Branch.ts +657 -0
- package/src/Contributions.ts +9 -0
- package/src/Credentials.ts +92 -0
- package/src/Database.ts +152 -0
- package/src/MySQL/MySQLBranch.ts +109 -0
- package/src/MySQL/MySQLClusterSize.ts +185 -0
- package/src/MySQL/MySQLDatabase.ts +521 -0
- package/src/MySQL/MySQLMigrations.ts +133 -0
- package/src/MySQL/MySQLOrigin.ts +15 -0
- package/src/MySQL/MySQLPassword.ts +428 -0
- package/src/MySQL/index.ts +5 -0
- package/src/OAuthClient.ts +59 -0
- package/src/Postgres/PostgresBranch.ts +97 -0
- package/src/Postgres/PostgresClusterSize.ts +262 -0
- package/src/Postgres/PostgresDatabase.ts +469 -0
- package/src/Postgres/PostgresDefaultRole.ts +340 -0
- package/src/Postgres/PostgresMigrations.ts +122 -0
- package/src/Postgres/PostgresOrigin.ts +15 -0
- package/src/Postgres/PostgresRole.ts +547 -0
- package/src/Postgres/index.ts +6 -0
- package/src/Providers.ts +67 -0
- package/src/Util.ts +205 -0
- package/src/index.ts +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,KAAK,oBAAoB,EAAE,KAAK,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClG,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * as Auth from "./AuthProvider.js";
|
|
2
|
+
export { makeBranchProvider } from "./Branch.js";
|
|
3
|
+
export { Credentials, CredentialsFromEnv, DEFAULT_API_BASE_URL, fromAuthProvider, fromToken, } from "./Credentials.js";
|
|
4
|
+
export * from "./Database.js";
|
|
5
|
+
export * from "./MySQL/index.js";
|
|
6
|
+
export * from "./Postgres/index.js";
|
|
7
|
+
export * from "./Providers.js";
|
|
8
|
+
export * from "./Util.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAmD,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAClG,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACpB,gBAAgB,EAChB,SAAS,GACV,MAAM,kBAAkB,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alchemy.run/planetscale",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "2.0.0-beta.85",
|
|
4
4
|
"description": "PlanetScale MySQL and Postgres databases for Alchemy.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -10,12 +10,88 @@
|
|
|
10
10
|
"planetscale",
|
|
11
11
|
"postgres"
|
|
12
12
|
],
|
|
13
|
+
"homepage": "https://alchemy.run",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/alchemy-run/alchemy/issues"
|
|
16
|
+
},
|
|
13
17
|
"license": "Apache-2.0",
|
|
14
18
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
15
|
-
"homepage": "https://alchemy.run",
|
|
16
19
|
"repository": {
|
|
17
20
|
"type": "git",
|
|
18
21
|
"url": "git+https://github.com/alchemy-run/alchemy.git",
|
|
19
22
|
"directory": "packages/planetscale"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib",
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./lib/index.d.ts",
|
|
33
|
+
"bun": "./src/index.ts",
|
|
34
|
+
"worker": "./src/index.ts",
|
|
35
|
+
"default": "./lib/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./MySQL": {
|
|
38
|
+
"types": "./lib/MySQL/index.d.ts",
|
|
39
|
+
"bun": "./src/MySQL/index.ts",
|
|
40
|
+
"worker": "./src/MySQL/index.ts",
|
|
41
|
+
"default": "./lib/MySQL/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./Postgres": {
|
|
44
|
+
"types": "./lib/Postgres/index.d.ts",
|
|
45
|
+
"bun": "./src/Postgres/index.ts",
|
|
46
|
+
"worker": "./src/Postgres/index.ts",
|
|
47
|
+
"default": "./lib/Postgres/index.js"
|
|
48
|
+
},
|
|
49
|
+
"./*": {
|
|
50
|
+
"types": "./lib/*.d.ts",
|
|
51
|
+
"bun": "./src/*.ts",
|
|
52
|
+
"worker": "./src/*.ts",
|
|
53
|
+
"default": "./lib/*.js"
|
|
54
|
+
},
|
|
55
|
+
"./package.json": "./package.json"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@alchemy.run/sql": "2.0.0-beta.85",
|
|
62
|
+
"@distilled.cloud/core": "1.0.0-rc.18",
|
|
63
|
+
"@distilled.cloud/planetscale": "1.0.0-rc.18"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/bun": "1.4.3",
|
|
67
|
+
"@types/pg": "^8.20.0",
|
|
68
|
+
"alchemy": "2.0.0-beta.85",
|
|
69
|
+
"effect": "^4.1.0",
|
|
70
|
+
"mysql2": "^3.24.2",
|
|
71
|
+
"pg": "^8.22.0",
|
|
72
|
+
"typescript": "^7.0.2"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"alchemy": "2.0.0-beta.85",
|
|
76
|
+
"effect": "^4.1.0",
|
|
77
|
+
"mysql2": "^3.24.2",
|
|
78
|
+
"pg": "^8.22.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependenciesMeta": {
|
|
81
|
+
"mysql2": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"pg": {
|
|
85
|
+
"optional": true
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"alchemy": {
|
|
89
|
+
"contributes": [
|
|
90
|
+
"@alchemy.run/planetscale/Contributions"
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"scripts": {
|
|
94
|
+
"build": "pnpm -r --filter \"{.}...\" run build:package",
|
|
95
|
+
"build:package": "node scripts/build.ts"
|
|
20
96
|
}
|
|
21
|
-
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
import { listOrganizations } from "@distilled.cloud/planetscale";
|
|
2
|
+
import * as PsCredentialsModule from "@distilled.cloud/planetscale/Credentials";
|
|
3
|
+
import {
|
|
4
|
+
AuthError,
|
|
5
|
+
AuthProviderLayer,
|
|
6
|
+
refreshHint,
|
|
7
|
+
type ConfigureField,
|
|
8
|
+
type ConfigureMethod,
|
|
9
|
+
NeedsReauth,
|
|
10
|
+
type ProviderDetails,
|
|
11
|
+
} from "alchemy/Auth/AuthProvider";
|
|
12
|
+
import { browserOAuth } from "alchemy/Auth/BrowserOAuth";
|
|
13
|
+
import { displayRedacted } from "alchemy/Auth/Credentials";
|
|
14
|
+
import { getEnvRedactedRequired, getEnvRequired, mapPromptCancellation } from "alchemy/Auth/Env";
|
|
15
|
+
import { withProfileCredentialsLock } from "alchemy/Auth/Lock";
|
|
16
|
+
import {
|
|
17
|
+
storedSecret,
|
|
18
|
+
storedValueText,
|
|
19
|
+
validateFieldValues,
|
|
20
|
+
} from "alchemy/Auth/StoredAuthProvider";
|
|
21
|
+
import * as Interaction from "alchemy/Interaction";
|
|
22
|
+
import * as Clock from "effect/Clock";
|
|
23
|
+
import * as Duration from "effect/Duration";
|
|
24
|
+
import * as Effect from "effect/Effect";
|
|
25
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
26
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
27
|
+
import * as Layer from "effect/Layer";
|
|
28
|
+
import * as Match from "effect/Match";
|
|
29
|
+
import * as Redacted from "effect/Redacted";
|
|
30
|
+
import * as Schema from "effect/Schema";
|
|
31
|
+
import * as OAuthClient from "./OAuthClient.ts";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Canonical name registered in {@link AuthProviders}. Use this key to look
|
|
35
|
+
* up the PlanetScale {@link AuthProvider} from inside provider Layers.
|
|
36
|
+
*/
|
|
37
|
+
export const PLANETSCALE_AUTH_PROVIDER_NAME = "Planetscale";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Provide PlanetScale `Credentials` + `HttpClient` to an Effect using a
|
|
41
|
+
* just-obtained OAuth access token. Used during configure to call
|
|
42
|
+
* org-discovery endpoints before the user has chosen an org.
|
|
43
|
+
*
|
|
44
|
+
* `organization` is required by the credential type but isn't consulted by
|
|
45
|
+
* `listOrganizations` (it's a user-scoped endpoint), so an empty string is
|
|
46
|
+
* fine here.
|
|
47
|
+
*/
|
|
48
|
+
const withOAuthCredentials = <A, E, R>(
|
|
49
|
+
accessToken: string,
|
|
50
|
+
effect: Effect.Effect<A, E, R | PsCredentialsModule.Credentials | HttpClient.HttpClient>,
|
|
51
|
+
): Effect.Effect<A, E, R> =>
|
|
52
|
+
Effect.provide(
|
|
53
|
+
effect,
|
|
54
|
+
Layer.mergeAll(
|
|
55
|
+
PsCredentialsModule.fromOAuth({
|
|
56
|
+
accessToken: Redacted.make(accessToken),
|
|
57
|
+
organization: "",
|
|
58
|
+
}),
|
|
59
|
+
FetchHttpClient.layer,
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List the organizations the OAuth user belongs to and either auto-pick
|
|
65
|
+
* (one org) or prompt the user to choose. Returns the org's URL slug
|
|
66
|
+
* (`name` field, used as `{organization}` in API paths).
|
|
67
|
+
*/
|
|
68
|
+
const selectOrganization = (accessToken: string) =>
|
|
69
|
+
Effect.gen(function* () {
|
|
70
|
+
const list = yield* listOrganizations;
|
|
71
|
+
const response = yield* list({});
|
|
72
|
+
const orgs = response.data;
|
|
73
|
+
if (orgs.length === 0) {
|
|
74
|
+
return yield* AuthError.make({
|
|
75
|
+
message: "Planetscale: no organizations found for this credential.",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (orgs.length === 1) {
|
|
79
|
+
const org = orgs[0];
|
|
80
|
+
if (org === undefined) {
|
|
81
|
+
return yield* AuthError.make({
|
|
82
|
+
message: "Planetscale: organization response was unexpectedly empty.",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
yield* Interaction.info(`Planetscale: using organization: ${org.name} (${org.id})`);
|
|
86
|
+
return org.name;
|
|
87
|
+
}
|
|
88
|
+
return yield* Interaction.select({
|
|
89
|
+
message: "Select a Planetscale organization",
|
|
90
|
+
options: orgs.map((o) => ({
|
|
91
|
+
value: o.name,
|
|
92
|
+
label: o.name,
|
|
93
|
+
description: o.id,
|
|
94
|
+
})),
|
|
95
|
+
}).pipe(mapPromptCancellation);
|
|
96
|
+
}).pipe((e) => withOAuthCredentials(accessToken, e));
|
|
97
|
+
|
|
98
|
+
const options: Array<{
|
|
99
|
+
value: PlanetscaleAuthConfig["method"];
|
|
100
|
+
label: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
}> = [
|
|
103
|
+
{
|
|
104
|
+
value: "oauth",
|
|
105
|
+
label: "OAuth",
|
|
106
|
+
description: "recommended — browser-based login with automatic token refresh",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
value: "stored",
|
|
110
|
+
label: "Service Token",
|
|
111
|
+
description: "enter a service token and store it inline in the provider file",
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Typed values stored in a PlanetScale provider profile document. Both
|
|
117
|
+
* service-token and OAuth values are inline. PlanetScale has no PKCE flow,
|
|
118
|
+
* so the OAuth application's
|
|
119
|
+
* `client_secret` ships in the CLI — see {@link OAuthClient}.
|
|
120
|
+
*/
|
|
121
|
+
export const PlanetscaleAuthConfigSchema = Schema.Union([
|
|
122
|
+
Schema.Struct({
|
|
123
|
+
method: Schema.Literal("stored"),
|
|
124
|
+
tokenId: Schema.String,
|
|
125
|
+
token: Schema.String,
|
|
126
|
+
organization: Schema.String,
|
|
127
|
+
}),
|
|
128
|
+
Schema.Struct({
|
|
129
|
+
method: Schema.Literal("oauth"),
|
|
130
|
+
organization: Schema.String,
|
|
131
|
+
clientId: Schema.optional(Schema.String),
|
|
132
|
+
access: Schema.String,
|
|
133
|
+
refresh: Schema.String,
|
|
134
|
+
expires: Schema.Number,
|
|
135
|
+
scopes: Schema.String.pipe(Schema.Array, Schema.mutable),
|
|
136
|
+
}),
|
|
137
|
+
]);
|
|
138
|
+
export type PlanetscaleAuthConfig = typeof PlanetscaleAuthConfigSchema.Type;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolved in-memory PlanetScale credentials returned by
|
|
142
|
+
* {@link AuthProviderImpl.read}. Either a service token (`tokenId`/`token`)
|
|
143
|
+
* or an OAuth access token.
|
|
144
|
+
*/
|
|
145
|
+
export type PlanetscaleResolvedCredentials =
|
|
146
|
+
| {
|
|
147
|
+
type: "apiToken";
|
|
148
|
+
tokenId: Redacted.Redacted<string>;
|
|
149
|
+
token: Redacted.Redacted<string>;
|
|
150
|
+
organization: string;
|
|
151
|
+
source: {
|
|
152
|
+
type: PlanetscaleAuthConfig["method"] | "env";
|
|
153
|
+
details?: string;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
| {
|
|
157
|
+
type: "oauth";
|
|
158
|
+
accessToken: Redacted.Redacted<string>;
|
|
159
|
+
expires: number;
|
|
160
|
+
organization: string;
|
|
161
|
+
source: {
|
|
162
|
+
type: PlanetscaleAuthConfig["method"] | "env";
|
|
163
|
+
details?: string;
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Layer that registers the PlanetScale {@link AuthProvider} into the
|
|
169
|
+
* {@link AuthProviders} registry when built. Include this in the
|
|
170
|
+
* PlanetScale `providers()` layer so the alchemy CLI can discover it.
|
|
171
|
+
*
|
|
172
|
+
* Supported methods:
|
|
173
|
+
* - `stored`: prompts for a service token and stores it inline.
|
|
174
|
+
* - `oauth`: browser-based login storing access/refresh values inline.
|
|
175
|
+
*/
|
|
176
|
+
export const PlanetscaleAuth = AuthProviderLayer<
|
|
177
|
+
PlanetscaleAuthConfig,
|
|
178
|
+
PlanetscaleResolvedCredentials
|
|
179
|
+
>()(
|
|
180
|
+
PLANETSCALE_AUTH_PROVIDER_NAME,
|
|
181
|
+
Effect.gen(function* () {
|
|
182
|
+
const oauthLogin = (_profileName: string) =>
|
|
183
|
+
Effect.gen(function* () {
|
|
184
|
+
const authorization = yield* OAuthClient.authorize();
|
|
185
|
+
|
|
186
|
+
const credentials = yield* browserOAuth({
|
|
187
|
+
provider: "Planetscale",
|
|
188
|
+
url: authorization.url,
|
|
189
|
+
callback: OAuthClient.callback(authorization),
|
|
190
|
+
exchange: (input) => OAuthClient.exchangeCallbackInput(input, authorization),
|
|
191
|
+
});
|
|
192
|
+
yield* Interaction.success("Planetscale: OAuth credentials saved.");
|
|
193
|
+
return credentials;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const configureOAuth = Effect.fn(function* (profileName: string) {
|
|
197
|
+
const oauthCreds = yield* oauthLogin(profileName);
|
|
198
|
+
|
|
199
|
+
// Use the just-issued access token to list the user's orgs and let
|
|
200
|
+
// them pick (mirrors Cloudflare's selectAccount). Requires the
|
|
201
|
+
// `user:read_organizations` scope. If the call fails for any
|
|
202
|
+
// reason — missing scope, network, off-spec response — fall back
|
|
203
|
+
// to a manual prompt so login still completes.
|
|
204
|
+
const organization = yield* oauthCreds.access.pipe(
|
|
205
|
+
Redacted.value,
|
|
206
|
+
selectOrganization,
|
|
207
|
+
Effect.catch((e) =>
|
|
208
|
+
Effect.gen(function* () {
|
|
209
|
+
yield* Interaction.warning(
|
|
210
|
+
`Planetscale: could not auto-list organizations (${String(e)}). Falling back to manual entry.`,
|
|
211
|
+
);
|
|
212
|
+
return yield* Interaction.text({
|
|
213
|
+
message: "Planetscale Organization (URL slug)",
|
|
214
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
215
|
+
}).pipe(mapPromptCancellation);
|
|
216
|
+
}),
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
method: "oauth" as const,
|
|
222
|
+
organization,
|
|
223
|
+
clientId: oauthCreds.clientId,
|
|
224
|
+
access: Redacted.value(oauthCreds.access),
|
|
225
|
+
refresh: Redacted.value(oauthCreds.refresh),
|
|
226
|
+
expires: oauthCreds.expires,
|
|
227
|
+
scopes: oauthCreds.scopes,
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const loginStored = Effect.fn(function* (_profileName: string) {
|
|
232
|
+
const tokenId = yield* Interaction.text({
|
|
233
|
+
message: "Planetscale Service Token ID",
|
|
234
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
235
|
+
}).pipe(mapPromptCancellation);
|
|
236
|
+
|
|
237
|
+
const token = yield* Interaction.password({
|
|
238
|
+
message: "Planetscale Service Token",
|
|
239
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
240
|
+
}).pipe(mapPromptCancellation);
|
|
241
|
+
|
|
242
|
+
const organization = yield* Interaction.text({
|
|
243
|
+
message: "Planetscale Organization (URL slug)",
|
|
244
|
+
validate: (v) => (v.length === 0 ? "Required" : undefined),
|
|
245
|
+
}).pipe(mapPromptCancellation);
|
|
246
|
+
|
|
247
|
+
yield* Interaction.success("Planetscale: credentials saved.");
|
|
248
|
+
return { method: "stored" as const, tokenId, token, organization };
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const configureInteractive = (profileName: string) =>
|
|
252
|
+
Interaction.select({
|
|
253
|
+
message: "Planetscale authentication method",
|
|
254
|
+
options,
|
|
255
|
+
}).pipe(
|
|
256
|
+
Effect.flatMap((method) =>
|
|
257
|
+
Match.value(method).pipe(
|
|
258
|
+
Match.when("oauth", () => configureOAuth(profileName)),
|
|
259
|
+
Match.when("stored", () => loginStored(profileName)),
|
|
260
|
+
Match.exhaustive,
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const configureCredentials = (profileName: string) =>
|
|
266
|
+
configureInteractive(profileName).pipe(
|
|
267
|
+
Effect.mapError((e) =>
|
|
268
|
+
AuthError.make({
|
|
269
|
+
message: "failed to configure credentials",
|
|
270
|
+
cause: e,
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Flag-driven (`--method stored --set ...`) fields, mirroring the
|
|
277
|
+
* interactive service-token prompts. OAuth requires a browser and stays
|
|
278
|
+
* interactive-only, so it is deliberately absent from
|
|
279
|
+
* {@link configureMethods}.
|
|
280
|
+
*/
|
|
281
|
+
const serviceTokenFields: ReadonlyArray<ConfigureField> = [
|
|
282
|
+
{ name: "tokenId", label: "Planetscale Service Token ID" },
|
|
283
|
+
{ name: "token", label: "Planetscale Service Token", secret: true },
|
|
284
|
+
{ name: "organization", label: "Planetscale Organization (URL slug)" },
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
const configureMethods: ReadonlyArray<ConfigureMethod> = [
|
|
288
|
+
{ method: "stored", fields: serviceTokenFields },
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const configureWith = (
|
|
292
|
+
_profileName: string,
|
|
293
|
+
input: {
|
|
294
|
+
readonly method: string;
|
|
295
|
+
readonly values: Record<string, string>;
|
|
296
|
+
},
|
|
297
|
+
): Effect.Effect<PlanetscaleAuthConfig, AuthError, Interaction.Interaction> =>
|
|
298
|
+
input.method === "stored"
|
|
299
|
+
? validateFieldValues(
|
|
300
|
+
PLANETSCALE_AUTH_PROVIDER_NAME,
|
|
301
|
+
serviceTokenFields,
|
|
302
|
+
input.values,
|
|
303
|
+
).pipe(
|
|
304
|
+
Effect.map((values) => ({
|
|
305
|
+
method: "stored" as const,
|
|
306
|
+
tokenId: Redacted.value(storedSecret(values.tokenId) ?? Redacted.make("")),
|
|
307
|
+
token: Redacted.value(storedSecret(values.token) ?? Redacted.make("")),
|
|
308
|
+
organization: storedValueText(values.organization) ?? "",
|
|
309
|
+
})),
|
|
310
|
+
Effect.tap(() => Interaction.success("Planetscale: credentials saved.")),
|
|
311
|
+
)
|
|
312
|
+
: Effect.fail(
|
|
313
|
+
AuthError.make({
|
|
314
|
+
message: `Planetscale: unknown method '${input.method}'. Valid methods: stored. (OAuth is interactive-only.)`,
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
const resolveCredentials = (
|
|
319
|
+
profileName: string,
|
|
320
|
+
config: PlanetscaleAuthConfig,
|
|
321
|
+
updateConfig?: (config: PlanetscaleAuthConfig) => Effect.Effect<void, AuthError>,
|
|
322
|
+
) =>
|
|
323
|
+
Effect.gen(function* () {
|
|
324
|
+
const reauth = refreshHint(PLANETSCALE_AUTH_PROVIDER_NAME, profileName);
|
|
325
|
+
return yield* Match.value(config).pipe(
|
|
326
|
+
Match.when({ method: "stored" }, (stored) =>
|
|
327
|
+
Effect.succeed({
|
|
328
|
+
type: "apiToken" as const,
|
|
329
|
+
tokenId: Redacted.make(stored.tokenId),
|
|
330
|
+
token: Redacted.make(stored.token),
|
|
331
|
+
organization: stored.organization,
|
|
332
|
+
source: { type: "stored" as const, details: undefined },
|
|
333
|
+
} satisfies PlanetscaleResolvedCredentials),
|
|
334
|
+
),
|
|
335
|
+
Match.when({ method: "oauth" }, (cfg) =>
|
|
336
|
+
Effect.gen(function* () {
|
|
337
|
+
const creds: OAuthClient.OAuthCredentials = {
|
|
338
|
+
type: "oauth",
|
|
339
|
+
clientId: cfg.clientId,
|
|
340
|
+
access: Redacted.make(cfg.access),
|
|
341
|
+
refresh: Redacted.make(cfg.refresh),
|
|
342
|
+
expires: cfg.expires,
|
|
343
|
+
scopes: cfg.scopes,
|
|
344
|
+
};
|
|
345
|
+
if (!OAuthClient.usesCurrentClient(creds)) {
|
|
346
|
+
return yield* NeedsReauth.make({
|
|
347
|
+
provider: PLANETSCALE_AUTH_PROVIDER_NAME,
|
|
348
|
+
profile: profileName,
|
|
349
|
+
message: `Planetscale OAuth credentials for profile '${profileName}' were issued to an incompatible OAuth client and have been removed. ${reauth}`,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
// Refresh proactively if the token has expired (or is within
|
|
353
|
+
// 10s of expiring). Persist the refreshed creds so subsequent
|
|
354
|
+
// resolves don't repeat the round-trip.
|
|
355
|
+
const now = yield* Clock.currentTimeMillis;
|
|
356
|
+
const fresh =
|
|
357
|
+
creds.expires > now + 10_000
|
|
358
|
+
? creds
|
|
359
|
+
: yield* OAuthClient.refresh(creds).pipe(
|
|
360
|
+
// Only the refresh round-trip maps to NeedsReauth — a
|
|
361
|
+
// failed persist afterwards is a local I/O AuthError and
|
|
362
|
+
// passes through untouched.
|
|
363
|
+
Effect.mapError((e) =>
|
|
364
|
+
NeedsReauth.make({
|
|
365
|
+
provider: PLANETSCALE_AUTH_PROVIDER_NAME,
|
|
366
|
+
profile: profileName,
|
|
367
|
+
message: `Planetscale OAuth refresh failed. ${reauth}`,
|
|
368
|
+
cause: e,
|
|
369
|
+
}),
|
|
370
|
+
),
|
|
371
|
+
);
|
|
372
|
+
if (fresh !== creds) {
|
|
373
|
+
yield* (
|
|
374
|
+
updateConfig?.({
|
|
375
|
+
method: "oauth",
|
|
376
|
+
organization: cfg.organization,
|
|
377
|
+
clientId: fresh.clientId,
|
|
378
|
+
access: Redacted.value(fresh.access),
|
|
379
|
+
refresh: Redacted.value(fresh.refresh),
|
|
380
|
+
expires: fresh.expires,
|
|
381
|
+
scopes: fresh.scopes,
|
|
382
|
+
}) ?? Effect.void
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
type: "oauth" as const,
|
|
387
|
+
accessToken: fresh.access,
|
|
388
|
+
expires: fresh.expires,
|
|
389
|
+
organization: cfg.organization,
|
|
390
|
+
source: { type: "oauth" as const },
|
|
391
|
+
} satisfies PlanetscaleResolvedCredentials;
|
|
392
|
+
}),
|
|
393
|
+
),
|
|
394
|
+
Match.exhaustive,
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const logout = (_profileName: string, config: PlanetscaleAuthConfig) =>
|
|
399
|
+
Match.value(config).pipe(
|
|
400
|
+
Match.when({ method: "stored" }, () => Effect.void),
|
|
401
|
+
Match.when({ method: "oauth" }, () => Effect.void),
|
|
402
|
+
Match.exhaustive,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const login = (
|
|
406
|
+
profileName: string,
|
|
407
|
+
config: PlanetscaleAuthConfig,
|
|
408
|
+
updateConfig?: (config: PlanetscaleAuthConfig) => Effect.Effect<void, AuthError>,
|
|
409
|
+
reload?: Effect.Effect<PlanetscaleAuthConfig, AuthError>,
|
|
410
|
+
) =>
|
|
411
|
+
Match.value(config).pipe(
|
|
412
|
+
Match.when({ method: "stored" }, (stored) => Effect.succeed(stored)),
|
|
413
|
+
Match.when({ method: "oauth" }, (oauth) =>
|
|
414
|
+
Effect.gen(function* () {
|
|
415
|
+
const credentials: OAuthClient.OAuthCredentials = {
|
|
416
|
+
type: "oauth",
|
|
417
|
+
clientId: oauth.clientId,
|
|
418
|
+
access: Redacted.make(oauth.access),
|
|
419
|
+
refresh: Redacted.make(oauth.refresh),
|
|
420
|
+
expires: oauth.expires,
|
|
421
|
+
scopes: oauth.scopes,
|
|
422
|
+
};
|
|
423
|
+
if (!OAuthClient.usesCurrentClient(credentials)) {
|
|
424
|
+
return yield* configureOAuth(profileName);
|
|
425
|
+
}
|
|
426
|
+
// Refresh tokens are single-use: refresh with the token stored
|
|
427
|
+
// now, read under the lock, not the one `config` was read with.
|
|
428
|
+
const outcome = yield* withProfileCredentialsLock(
|
|
429
|
+
profileName,
|
|
430
|
+
Effect.gen(function* () {
|
|
431
|
+
const current = yield* reload ?? Effect.succeed(oauth);
|
|
432
|
+
if (current.method !== "oauth") return { type: "browser" as const };
|
|
433
|
+
const currentCredentials: OAuthClient.OAuthCredentials = {
|
|
434
|
+
type: "oauth",
|
|
435
|
+
clientId: current.clientId,
|
|
436
|
+
access: Redacted.make(current.access),
|
|
437
|
+
refresh: Redacted.make(current.refresh),
|
|
438
|
+
expires: current.expires,
|
|
439
|
+
scopes: current.scopes,
|
|
440
|
+
};
|
|
441
|
+
if (!OAuthClient.usesCurrentClient(currentCredentials)) {
|
|
442
|
+
return { type: "browser" as const };
|
|
443
|
+
}
|
|
444
|
+
return yield* Interaction.info("Planetscale: refreshing OAuth credentials...").pipe(
|
|
445
|
+
Effect.andThen(OAuthClient.refresh(currentCredentials)),
|
|
446
|
+
Effect.flatMap((credentials) => {
|
|
447
|
+
const config = {
|
|
448
|
+
...current,
|
|
449
|
+
clientId: credentials.clientId,
|
|
450
|
+
access: Redacted.value(credentials.access),
|
|
451
|
+
refresh: Redacted.value(credentials.refresh),
|
|
452
|
+
expires: credentials.expires,
|
|
453
|
+
scopes: credentials.scopes,
|
|
454
|
+
};
|
|
455
|
+
// Saved here, under the lock, when the caller can take it;
|
|
456
|
+
// otherwise handed back for the caller to save.
|
|
457
|
+
const unsaved: Effect.Effect<typeof config | undefined, AuthError> =
|
|
458
|
+
updateConfig === undefined
|
|
459
|
+
? Effect.succeed(config)
|
|
460
|
+
: Effect.as(updateConfig(config), undefined);
|
|
461
|
+
return unsaved.pipe(
|
|
462
|
+
Effect.map((config) => ({ type: "refreshed" as const, config })),
|
|
463
|
+
);
|
|
464
|
+
}),
|
|
465
|
+
Effect.tap(() =>
|
|
466
|
+
Interaction.success("Planetscale: OAuth credentials refreshed."),
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
}),
|
|
470
|
+
).pipe(
|
|
471
|
+
Effect.catchTag(
|
|
472
|
+
[
|
|
473
|
+
"OAuthError",
|
|
474
|
+
"OAuthNetworkError",
|
|
475
|
+
"OAuthServerUnavailable",
|
|
476
|
+
"OAuthInvalidResponse",
|
|
477
|
+
],
|
|
478
|
+
() => Effect.succeed({ type: "browser" as const }),
|
|
479
|
+
),
|
|
480
|
+
);
|
|
481
|
+
if (outcome.type === "browser") {
|
|
482
|
+
return yield* configureOAuth(profileName);
|
|
483
|
+
}
|
|
484
|
+
return outcome.config;
|
|
485
|
+
}),
|
|
486
|
+
),
|
|
487
|
+
Match.exhaustive,
|
|
488
|
+
Effect.mapError((e) => AuthError.make({ message: "login failed", cause: e })),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const details = (
|
|
492
|
+
profileName: string,
|
|
493
|
+
config: PlanetscaleAuthConfig,
|
|
494
|
+
updateConfig?: (config: PlanetscaleAuthConfig) => Effect.Effect<void, AuthError>,
|
|
495
|
+
) =>
|
|
496
|
+
Effect.all([
|
|
497
|
+
resolveCredentials(profileName, config, updateConfig),
|
|
498
|
+
Clock.currentTimeMillis,
|
|
499
|
+
]).pipe(
|
|
500
|
+
Effect.map(([creds, now]): ProviderDetails => {
|
|
501
|
+
const source: PlanetscaleResolvedCredentials["source"] = creds.source;
|
|
502
|
+
const sourceStr = source.details ? `${source.type} - ${source.details}` : source.type;
|
|
503
|
+
return Match.value(creds).pipe(
|
|
504
|
+
Match.when({ type: "apiToken" }, (c) => ({
|
|
505
|
+
lines: [
|
|
506
|
+
{ key: "tokenId", value: displayRedacted(c.tokenId, 3) },
|
|
507
|
+
{ key: "token", value: displayRedacted(c.token, 6) },
|
|
508
|
+
{ key: "organization", value: c.organization },
|
|
509
|
+
{ key: "source", value: sourceStr },
|
|
510
|
+
],
|
|
511
|
+
})),
|
|
512
|
+
Match.when({ type: "oauth" }, (c) => {
|
|
513
|
+
const remainingMs = c.expires - now;
|
|
514
|
+
const expiresAt = new Date(c.expires).toISOString();
|
|
515
|
+
const expiresStr =
|
|
516
|
+
remainingMs <= 0
|
|
517
|
+
? `expired (${expiresAt})`
|
|
518
|
+
: `in ${Duration.format(Duration.millis(remainingMs))} (${expiresAt})`;
|
|
519
|
+
return {
|
|
520
|
+
lines: [
|
|
521
|
+
{ key: "accessToken", value: displayRedacted(c.accessToken) },
|
|
522
|
+
{ key: "expires", value: expiresStr },
|
|
523
|
+
{ key: "organization", value: c.organization },
|
|
524
|
+
{ key: "source", value: sourceStr },
|
|
525
|
+
],
|
|
526
|
+
};
|
|
527
|
+
}),
|
|
528
|
+
Match.exhaustive,
|
|
529
|
+
);
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const readEnvironment = Effect.gen(function* () {
|
|
534
|
+
const tokenId = yield* getEnvRedactedRequired("PLANETSCALE_API_TOKEN_ID");
|
|
535
|
+
const token = yield* getEnvRedactedRequired("PLANETSCALE_API_TOKEN");
|
|
536
|
+
const organization = yield* getEnvRequired("PLANETSCALE_ORGANIZATION");
|
|
537
|
+
return {
|
|
538
|
+
type: "apiToken" as const,
|
|
539
|
+
tokenId,
|
|
540
|
+
token,
|
|
541
|
+
organization,
|
|
542
|
+
source: {
|
|
543
|
+
type: "env" as const,
|
|
544
|
+
details: "PLANETSCALE_API_TOKEN_ID/PLANETSCALE_API_TOKEN",
|
|
545
|
+
},
|
|
546
|
+
} satisfies PlanetscaleResolvedCredentials;
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
configSchema: PlanetscaleAuthConfigSchema,
|
|
551
|
+
configure: configureCredentials,
|
|
552
|
+
configureWith,
|
|
553
|
+
configureMethods,
|
|
554
|
+
logout,
|
|
555
|
+
login,
|
|
556
|
+
details,
|
|
557
|
+
read: resolveCredentials,
|
|
558
|
+
readEnvironment,
|
|
559
|
+
environment: [
|
|
560
|
+
{
|
|
561
|
+
name: "PLANETSCALE_API_TOKEN_ID",
|
|
562
|
+
required: true,
|
|
563
|
+
description: "Service token id.",
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: "PLANETSCALE_API_TOKEN",
|
|
567
|
+
required: true,
|
|
568
|
+
secret: true,
|
|
569
|
+
description: "Service token secret.",
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
name: "PLANETSCALE_ORGANIZATION",
|
|
573
|
+
required: true,
|
|
574
|
+
description: "Organization URL slug.",
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
name: "PLANETSCALE_API_BASE_URL",
|
|
578
|
+
required: false,
|
|
579
|
+
description: "API base URL override.",
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
};
|
|
583
|
+
}),
|
|
584
|
+
);
|