@samebase/alchemy-convex 0.23.0 → 0.25.0
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/README.md +25 -17
- package/lib/{HubClient-B6hjkbjW.js → HubClient-Bs5_j18U.js} +45 -13
- package/lib/{HubClient-B6hjkbjW.js.map → HubClient-Bs5_j18U.js.map} +1 -1
- package/lib/cli.js +2 -2
- package/lib/cli.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +33 -21
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/HubClient.ts +70 -25
- package/src/SharedHubClient.ts +48 -30
- package/src/State.ts +1 -1
- package/src/cli.ts +7 -6
package/src/SharedHubClient.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Convex.SharedHubClient: one deployment of an app as a client of the central identity hub, for an
|
|
2
2
|
// app that deploys in its own GitHub Actions run, on any host.
|
|
3
3
|
//
|
|
4
|
-
// The run asks
|
|
5
|
-
// Samebase
|
|
6
|
-
// origin of the return address, which must name the same repository, and registers the client on
|
|
4
|
+
// The run asks the hub for a new client with a GitHub OIDC token, so the stack needs no key and no
|
|
5
|
+
// Samebase account. The token names the repository. The hub reads `/samebase-verification.json` at
|
|
6
|
+
// the origin of the return address, which must name the same repository, and registers the client on
|
|
7
7
|
// that address. The answer holds the client's own registration access token (RFC 7591), and each
|
|
8
8
|
// later deploy reads the client on the hub with that token (RFC 7592). The hub keeps only hashes, so
|
|
9
9
|
// the state is the only record of the secret and the token.
|
|
@@ -75,7 +75,7 @@ class SharedHubClientFailed extends Schema.TaggedError<SharedHubClientFailed>()(
|
|
|
75
75
|
|
|
76
76
|
const OidcToken = Schema.Struct({ value: Schema.NonEmptyString });
|
|
77
77
|
|
|
78
|
-
/** The 201 answer of `POST /
|
|
78
|
+
/** The 201 answer of `POST /hub-clients`. */
|
|
79
79
|
const NewClient = Schema.Struct({
|
|
80
80
|
issuer: Schema.NonEmptyString,
|
|
81
81
|
client_id: Schema.NonEmptyString,
|
|
@@ -83,46 +83,61 @@ const NewClient = Schema.Struct({
|
|
|
83
83
|
registration_access_token: Schema.NonEmptyString,
|
|
84
84
|
registration_client_uri: Schema.NonEmptyString,
|
|
85
85
|
redirect_uri: Schema.NonEmptyString,
|
|
86
|
+
/** The terms of the shared sign-in service, which the log shows once, for the new client. */
|
|
87
|
+
terms: Schema.NonEmptyString,
|
|
86
88
|
});
|
|
87
89
|
|
|
88
|
-
/** The other answers of `POST /
|
|
90
|
+
/** The other answers of `POST /hub-clients`, by status. */
|
|
89
91
|
const REFUSALS: Readonly<Record<number, string>> = {
|
|
90
|
-
400: "
|
|
91
|
-
401: "
|
|
92
|
-
403: "
|
|
93
|
-
404: "
|
|
94
|
-
429: "
|
|
95
|
-
502: "
|
|
92
|
+
400: "the identity hub refused the request: the return address must be https on a DNS name, and the app name at most 64 characters",
|
|
93
|
+
401: "the identity hub refused the GitHub OIDC token",
|
|
94
|
+
403: "the identity hub does not allow this repository or return address",
|
|
95
|
+
404: "the identity hub has no /hub-clients",
|
|
96
|
+
429: "the identity hub limits new clients",
|
|
97
|
+
502: "GitHub's keys are not available to the identity hub",
|
|
96
98
|
};
|
|
97
99
|
|
|
98
100
|
/** The reasons of a 403 that a log line shows. Any other text of the answer is never logged. */
|
|
99
101
|
const NOT_ALLOWED = new Map([
|
|
100
|
-
["
|
|
101
|
-
["
|
|
102
|
-
["
|
|
103
|
-
["
|
|
102
|
+
["paused", "the identity hub pauses new clients of third-party apps"],
|
|
103
|
+
["run_not_allowed", "the identity hub gives clients only to runs of main and of pull requests"],
|
|
104
|
+
["blocked", "Samebase blocked this repository or its owner"],
|
|
105
|
+
["suspended", "the identity hub suspended this repository"],
|
|
106
|
+
["too_many_apps", "the repository has as many apps on the identity hub as it may"],
|
|
107
|
+
["too_many_clients", "the app has as many clients on the identity hub as it may"],
|
|
108
|
+
["app_taken", "the app's name belongs to another app on the identity hub"],
|
|
109
|
+
["host_unreachable", "the identity hub could not reach the host of the return address"],
|
|
104
110
|
[
|
|
105
111
|
"verification_file_missing",
|
|
106
112
|
"the host of the return address does not serve /samebase-verification.json",
|
|
107
113
|
],
|
|
108
114
|
["verification_file_invalid", "/samebase-verification.json on the host holds no repository id"],
|
|
109
115
|
["repository_mismatch", "/samebase-verification.json on the host names another repository"],
|
|
110
|
-
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
/** The windows of a 429, by the name that the answer gives. */
|
|
119
|
+
const RATE_LIMITED = new Map([
|
|
120
|
+
["repository", "the identity hub gives one repository at most 30 new clients an hour"],
|
|
121
|
+
["owner", "the identity hub gives one owner at most 100 new clients an hour"],
|
|
122
|
+
["new_owners", "the identity hub takes at most 50 new owners an hour"],
|
|
111
123
|
]);
|
|
112
124
|
|
|
113
125
|
const Refusal = Schema.Struct({ reason: Schema.String });
|
|
126
|
+
const RateLimited = Schema.Struct({ window: Schema.String });
|
|
114
127
|
|
|
115
|
-
/** The text of a refusal: a known reason of a 403, else the text of its status. */
|
|
128
|
+
/** The text of a refusal: a known reason of a 403 or window of a 429, else the text of its status. */
|
|
116
129
|
const refusalOf = (status: number, body: unknown) => {
|
|
117
|
-
const
|
|
130
|
+
const known =
|
|
118
131
|
status === 403
|
|
119
|
-
?
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
? NOT_ALLOWED.get(
|
|
133
|
+
Option.getOrUndefined(Schema.decodeUnknownOption(Refusal)(body))?.reason ?? "",
|
|
134
|
+
)
|
|
135
|
+
: status === 429
|
|
136
|
+
? RATE_LIMITED.get(
|
|
137
|
+
Option.getOrUndefined(Schema.decodeUnknownOption(RateLimited)(body))?.window ?? "",
|
|
138
|
+
)
|
|
139
|
+
: undefined;
|
|
140
|
+
return known ?? REFUSALS[status] ?? "the identity hub refused the request";
|
|
126
141
|
};
|
|
127
142
|
|
|
128
143
|
const send = (who: string, url: string, token: string, init: { method: string; body?: string }) =>
|
|
@@ -167,8 +182,8 @@ const requestNewClient = (props: SharedHubClientProps, requestUrl: string, reque
|
|
|
167
182
|
Effect.gen(function* () {
|
|
168
183
|
const token = yield* readOidcToken(requestUrl, requestToken);
|
|
169
184
|
const { body, status } = yield* send(
|
|
170
|
-
"
|
|
171
|
-
`${process.env["
|
|
185
|
+
"The identity hub",
|
|
186
|
+
`${process.env["HUB_URL"] ?? "https://auth.samebase.com"}/hub-clients`,
|
|
172
187
|
token,
|
|
173
188
|
{
|
|
174
189
|
method: "POST",
|
|
@@ -184,7 +199,10 @@ const requestNewClient = (props: SharedHubClientProps, requestUrl: string, reque
|
|
|
184
199
|
reason: `${refusalOf(status, body)} (HTTP ${status})`,
|
|
185
200
|
});
|
|
186
201
|
}
|
|
187
|
-
const client = yield* decode(NewClient, "
|
|
202
|
+
const client = yield* decode(NewClient, "The identity hub")(body);
|
|
203
|
+
yield* Effect.logInfo(
|
|
204
|
+
`Convex.SharedHubClient: ${props.app} on ${client.redirect_uri} has a new client of the identity hub. Its use is under ${client.terms}.`,
|
|
205
|
+
);
|
|
188
206
|
return {
|
|
189
207
|
status: "on" as const,
|
|
190
208
|
issuer: client.issuer,
|
|
@@ -212,8 +230,8 @@ const newClient = (props: SharedHubClientProps) => {
|
|
|
212
230
|
};
|
|
213
231
|
const requestUrl = process.env["ACTIONS_ID_TOKEN_REQUEST_URL"];
|
|
214
232
|
const requestToken = process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"];
|
|
215
|
-
// A laptop, or
|
|
216
|
-
//
|
|
233
|
+
// A laptop, or another CI. In a workflow, `id-token: write` gives both, so a workflow without them
|
|
234
|
+
// is a mistake to show, not a normal run.
|
|
217
235
|
if (!requestUrl || !requestToken) {
|
|
218
236
|
return (
|
|
219
237
|
process.env["GITHUB_ACTIONS"] === "true"
|
package/src/State.ts
CHANGED
|
@@ -173,7 +173,7 @@ export interface FunctionApi {
|
|
|
173
173
|
/** POST /api/function, the call that `convex run` makes: the reply, which can be a function error. */
|
|
174
174
|
readonly call: (
|
|
175
175
|
path: string,
|
|
176
|
-
args: Record<string, string | RunRecord | undefined>,
|
|
176
|
+
args: Record<string, string | boolean | RunRecord | undefined>,
|
|
177
177
|
) => Effect.Effect<Reply, StateStoreError>;
|
|
178
178
|
}
|
|
179
179
|
|
package/src/cli.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// alchemy-convex hub-client --team <team> --app <app> --callback-path <path>
|
|
5
5
|
// alchemy-convex stack-comment [--team <team>]
|
|
6
6
|
//
|
|
7
|
-
// hub-client runs in the directory of a Convex app, and
|
|
8
|
-
//
|
|
9
|
-
// runs as a step of a pull request workflow after
|
|
10
|
-
// of the repository: see src/StackComment.ts. It
|
|
11
|
-
// no --team. Both use CONVEX_ACCESS_TOKEN or the
|
|
7
|
+
// hub-client runs in the directory of a Convex app at each dev start, and renews the client of the
|
|
8
|
+
// identity hub of the team that the development deployment of that directory has, or gives it a new
|
|
9
|
+
// one: see registerDevelopmentClient. stack-comment runs as a step of a pull request workflow after
|
|
10
|
+
// the deploys, and keeps one comment with the stacks of the repository: see src/StackComment.ts. It
|
|
11
|
+
// needs GITHUB_TOKEN and, with a team access token, no --team. Both use CONVEX_ACCESS_TOKEN or the
|
|
12
|
+
// Convex CLI login.
|
|
12
13
|
import { execFileSync } from "node:child_process";
|
|
13
14
|
import { parseArgs } from "node:util";
|
|
14
15
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
@@ -49,7 +50,7 @@ const hubClient = (hub: { team: string; app: string; callbackPath: string; envPr
|
|
|
49
50
|
});
|
|
50
51
|
console.log(
|
|
51
52
|
callbackUrl === undefined
|
|
52
|
-
? "Hub client
|
|
53
|
+
? "Hub client renewed."
|
|
53
54
|
: `Hub client registered for ${callbackUrl}.`,
|
|
54
55
|
);
|
|
55
56
|
}).pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));
|