@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.
@@ -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 samebase.com for a new client with a GitHub OIDC token, so the stack needs no
5
- // Samebase key. The token names the repository. Samebase reads `/samebase-verification.json` at the
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 /api/hub-clients`. */
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 /api/hub-clients`, by status. */
90
+ /** The other answers of `POST /hub-clients`, by status. */
89
91
  const REFUSALS: Readonly<Record<number, string>> = {
90
- 400: "Samebase refused the request: the return address must be https on a DNS name",
91
- 401: "Samebase refused the GitHub OIDC token",
92
- 403: "Samebase does not allow this repository or return address",
93
- 404: "hub clients are off on Samebase",
94
- 429: "Samebase limits the requests of this repository",
95
- 502: "the identity hub is not available",
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
- ["run_not_allowed", "Samebase gives clients only to runs of main and of pull requests"],
101
- ["repository_not_connected", "the repository is not connected to Samebase"],
102
- ["owner_mismatch", "the repository's owner is not the owner connected to Samebase"],
103
- ["host_unreachable", "Samebase could not reach the host of the return address"],
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
- ["hub_refused", "the identity hub refused this repository"],
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 reason =
130
+ const known =
118
131
  status === 403
119
- ? Option.getOrUndefined(Schema.decodeUnknownOption(Refusal)(body))?.reason
120
- : undefined;
121
- return (
122
- (reason === undefined ? undefined : NOT_ALLOWED.get(reason)) ??
123
- REFUSALS[status] ??
124
- "Samebase refused the request"
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
- "Samebase",
171
- `${process.env["SAMEBASE_URL"] ?? "https://samebase.com"}/api/hub-clients`,
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, "Samebase")(body);
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 a repository outside Samebase. In a workflow, `id-token: write` gives both, so a
216
- // workflow without them is a mistake to show, not a normal run.
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 gives the development deployment of that
8
- // directory a client of the identity hub of the team: see registerDevelopmentClient. stack-comment
9
- // runs as a step of a pull request workflow after the deploys, and keeps one comment with the stacks
10
- // of the repository: see src/StackComment.ts. It needs GITHUB_TOKEN and, with a team access token,
11
- // no --team. Both use CONVEX_ACCESS_TOKEN or the Convex CLI login.
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 already registered."
53
+ ? "Hub client renewed."
53
54
  : `Hub client registered for ${callbackUrl}.`,
54
55
  );
55
56
  }).pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));