@samebase/alchemy-convex 0.23.0 → 0.24.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 CHANGED
@@ -309,16 +309,17 @@ call.
309
309
  destroy deletes the client.
310
310
 
311
311
  - `Convex.SharedHubClient`: `{ app, redirectUri, deploymentType, softwareVersion? }` gets a client
312
- of the central identity hub through samebase.com, for an app that deploys in its own GitHub
313
- Actions run, on any host. The workflow needs `permissions: id-token: write` and no Samebase key.
314
- The origin of `redirectUri` must serve `/samebase-verification.json` with the numeric GitHub id of
315
- the repository, `{ "repository": "123" }`, which `gh api repos/<owner>/<name> --jq .id` prints.
316
- Samebase reads it before it registers the client, so the site must be live first. Outputs
317
- `status`, `issuer`, `clientId`, `clientSecret`, `registrationAccessToken`,
318
- `registrationClientUri`, `deploymentType`, and `redirectUri`.
312
+ of the central identity hub, for an app that deploys in its own GitHub Actions run, on any host.
313
+ The workflow needs `permissions: id-token: write`, and no key and no Samebase account. `app` is at
314
+ most 64 characters. The origin of `redirectUri` must serve `/samebase-verification.json` with the
315
+ numeric GitHub id of the repository, `{ "repository": "123" }`, which
316
+ `gh api repos/<owner>/<name> --jq .id` prints. The hub reads it before it registers the client, so
317
+ the site must be live first. Outputs `status`, `issuer`, `clientId`, `clientSecret`,
318
+ `registrationAccessToken`, `registrationClientUri`, `deploymentType`, and `redirectUri`.
319
319
  - Each deploy reads the saved client on the hub with its own token. A client that is gone gets a
320
320
  new one, and a client on another `redirectUri` too.
321
- - A refusal logs its reason, such as a host that does not serve the file.
321
+ - A new client logs the link to the terms of the shared sign-in service. A refusal logs its
322
+ reason, such as a host that does not serve the file, a block, or the hourly limit it reached.
322
323
  - No failure stops a deploy or a destroy. A failed renewal keeps the saved values. Without saved
323
324
  values, `status` is `off`, and `issuer`, `clientId`, and `clientSecret` are the `no-hub` values
324
325
  of a checkout without a hub. A run without a GitHub OIDC token, such as on a laptop, does the
@@ -326,7 +327,7 @@ call.
326
327
  - `status` is `suspended` when the hub suspended the client. The client keeps its values.
327
328
  - A destroy deletes a preview client. It does nothing to a production client, because the
328
329
  production deployment can stay.
329
- - `SAMEBASE_URL` replaces `https://samebase.com`.
330
+ - `HUB_URL` replaces `https://auth.samebase.com`, the hub that the run asks at `/hub-clients`.
330
331
 
331
332
  A development deployment has no stack, so the package has a command for it. Run it in the directory
332
333
  of the app, before the first push: it registers the deployment of that directory with
package/lib/index.d.ts CHANGED
@@ -121,7 +121,7 @@ export declare class Providers extends Providers_base {}
121
121
  * default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login, and
122
122
  * `CONVEX_DEPLOY_KEY`).
123
123
  */
124
- export declare const providers: (credentials?: Layer.Layer<Credentials>) => Layer.Layer<Providers, never, import("effect/FileSystem").FileSystem | import("effect/Path").Path | import("effect/process/ChildProcessSpawner").ChildProcessSpawner>;
124
+ export declare const providers: (credentials?: Layer.Layer<Credentials>) => Layer.Layer<Providers, never, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem | import("effect/Path").Path>;
125
125
  //#endregion
126
126
  //#region src/Push.d.ts
127
127
  /** A value of the `env` prop: a string, or a secret such as `Config.Redacted("AUTH_SECRET")`. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Component.ts","../src/Project.ts","../src/HubClient.ts","../src/RunRecord.ts","../src/SharedHubClient.ts","../src/State.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Component.ts","../src/Project.ts","../src/HubClient.ts","../src/RunRecord.ts","../src/SharedHubClient.ts","../src/State.ts"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -41,40 +41,51 @@ const SharedHubClient = Resource("IdentityHub.SharedClient");
41
41
  /** One request gave no usable answer. The reason holds no secret. */
42
42
  var SharedHubClientFailed = class extends Schema.TaggedError()("SharedHubClientFailed", { reason: Schema.String }) {};
43
43
  const OidcToken = Schema.Struct({ value: Schema.NonEmptyString });
44
- /** The 201 answer of `POST /api/hub-clients`. */
44
+ /** The 201 answer of `POST /hub-clients`. */
45
45
  const NewClient = Schema.Struct({
46
46
  issuer: Schema.NonEmptyString,
47
47
  client_id: Schema.NonEmptyString,
48
48
  client_secret: Schema.NonEmptyString,
49
49
  registration_access_token: Schema.NonEmptyString,
50
50
  registration_client_uri: Schema.NonEmptyString,
51
- redirect_uri: Schema.NonEmptyString
51
+ redirect_uri: Schema.NonEmptyString,
52
+ /** The terms of the shared sign-in service, which the log shows once, for the new client. */
53
+ terms: Schema.NonEmptyString
52
54
  });
53
- /** The other answers of `POST /api/hub-clients`, by status. */
55
+ /** The other answers of `POST /hub-clients`, by status. */
54
56
  const REFUSALS = {
55
- 400: "Samebase refused the request: the return address must be https on a DNS name",
56
- 401: "Samebase refused the GitHub OIDC token",
57
- 403: "Samebase does not allow this repository or return address",
58
- 404: "hub clients are off on Samebase",
59
- 429: "Samebase limits the requests of this repository",
60
- 502: "the identity hub is not available"
57
+ 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",
58
+ 401: "the identity hub refused the GitHub OIDC token",
59
+ 403: "the identity hub does not allow this repository or return address",
60
+ 404: "the identity hub has no /hub-clients",
61
+ 429: "the identity hub limits new clients",
62
+ 502: "GitHub's keys are not available to the identity hub"
61
63
  };
62
64
  /** The reasons of a 403 that a log line shows. Any other text of the answer is never logged. */
63
65
  const NOT_ALLOWED = /* @__PURE__ */ new Map([
64
- ["run_not_allowed", "Samebase gives clients only to runs of main and of pull requests"],
65
- ["repository_not_connected", "the repository is not connected to Samebase"],
66
- ["owner_mismatch", "the repository's owner is not the owner connected to Samebase"],
67
- ["host_unreachable", "Samebase could not reach the host of the return address"],
66
+ ["paused", "the identity hub pauses new clients of third-party apps"],
67
+ ["run_not_allowed", "the identity hub gives clients only to runs of main and of pull requests"],
68
+ ["blocked", "Samebase blocked this repository or its owner"],
69
+ ["suspended", "the identity hub suspended this repository"],
70
+ ["too_many_apps", "the repository has as many apps on the identity hub as it may"],
71
+ ["too_many_clients", "the app has as many clients on the identity hub as it may"],
72
+ ["app_taken", "the app's name belongs to another app on the identity hub"],
73
+ ["host_unreachable", "the identity hub could not reach the host of the return address"],
68
74
  ["verification_file_missing", "the host of the return address does not serve /samebase-verification.json"],
69
75
  ["verification_file_invalid", "/samebase-verification.json on the host holds no repository id"],
70
- ["repository_mismatch", "/samebase-verification.json on the host names another repository"],
71
- ["hub_refused", "the identity hub refused this repository"]
76
+ ["repository_mismatch", "/samebase-verification.json on the host names another repository"]
77
+ ]);
78
+ /** The windows of a 429, by the name that the answer gives. */
79
+ const RATE_LIMITED = /* @__PURE__ */ new Map([
80
+ ["repository", "the identity hub gives one repository at most 30 new clients an hour"],
81
+ ["owner", "the identity hub gives one owner at most 100 new clients an hour"],
82
+ ["new_owners", "the identity hub takes at most 50 new owners an hour"]
72
83
  ]);
73
84
  const Refusal = Schema.Struct({ reason: Schema.String });
74
- /** The text of a refusal: a known reason of a 403, else the text of its status. */
85
+ const RateLimited = Schema.Struct({ window: Schema.String });
86
+ /** The text of a refusal: a known reason of a 403 or window of a 429, else the text of its status. */
75
87
  const refusalOf = (status, body) => {
76
- const reason = status === 403 ? Option.getOrUndefined(Schema.decodeUnknownOption(Refusal)(body))?.reason : void 0;
77
- return (reason === void 0 ? void 0 : NOT_ALLOWED.get(reason)) ?? REFUSALS[status] ?? "Samebase refused the request";
88
+ return (status === 403 ? NOT_ALLOWED.get(Option.getOrUndefined(Schema.decodeUnknownOption(Refusal)(body))?.reason ?? "") : status === 429 ? RATE_LIMITED.get(Option.getOrUndefined(Schema.decodeUnknownOption(RateLimited)(body))?.window ?? "") : void 0) ?? REFUSALS[status] ?? "the identity hub refused the request";
78
89
  };
79
90
  const send = (who, url, token, init) => Effect.tryPromise({
80
91
  try: async () => {
@@ -103,7 +114,7 @@ const readOidcToken = (requestUrl, requestToken) => Effect.gen(function* () {
103
114
  });
104
115
  const requestNewClient = (props, requestUrl, requestToken) => Effect.gen(function* () {
105
116
  const token = yield* readOidcToken(requestUrl, requestToken);
106
- const { body, status } = yield* send("Samebase", `${process.env["SAMEBASE_URL"] ?? "https://samebase.com"}/api/hub-clients`, token, {
117
+ const { body, status } = yield* send("The identity hub", `${process.env["HUB_URL"] ?? "https://auth.samebase.com"}/hub-clients`, token, {
107
118
  method: "POST",
108
119
  body: JSON.stringify({
109
120
  app: props.app,
@@ -112,7 +123,8 @@ const requestNewClient = (props, requestUrl, requestToken) => Effect.gen(functio
112
123
  })
113
124
  });
114
125
  if (status !== 201) return yield* new SharedHubClientFailed({ reason: `${refusalOf(status, body)} (HTTP ${status})` });
115
- const client = yield* decode(NewClient, "Samebase")(body);
126
+ const client = yield* decode(NewClient, "The identity hub")(body);
127
+ yield* Effect.logInfo(`Convex.SharedHubClient: ${props.app} on ${client.redirect_uri} has a new client of the identity hub. Its use is under ${client.terms}.`);
116
128
  return {
117
129
  status: "on",
118
130
  issuer: client.issuer,
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/Component.ts","../src/SharedHubClient.ts","../src/Providers.ts"],"sourcesContent":["import { toPath } from \"alchemy/FQN\";\nimport type { NamespaceNode } from \"alchemy/Namespace\";\nimport { Stack } from \"alchemy/Stack\";\nimport * as Effect from \"effect/Effect\";\nimport * as Schema from \"effect/Schema\";\nimport type { Deployment } from \"./Deployment.ts\";\n\n/** A component's resources occupy this namespace in the deployment's Alchemy stack and stage. */\nexport interface ComponentBinding {\n readonly type: \"component\";\n readonly path: string;\n readonly namespace: readonly string[];\n}\n\n/**\n * Associate a native namespace with an installed component. `component` is the\n * installed name from the app's `convex.config.ts`; a nested install joins the\n * names with slashes, such as `\"workspace/mail\"`. An undefined namespace\n * associates the stack root. This records display metadata; it does not install\n * the component, set its environment, or change removal policies.\n */\nexport const bindComponent = (options: {\n deployment: Deployment;\n component: string;\n namespace: NamespaceNode | undefined;\n}) =>\n Effect.gen(function* () {\n if (options.component === \"\") {\n return yield* Effect.die(new Error(\"The component name is empty.\"));\n }\n const namespace = toPath(options.namespace);\n const sid = `convex.component:${JSON.stringify(namespace)}`;\n const stack = yield* Stack;\n const previous = stack.bindings[options.deployment.FQN]?.find((binding) => binding.sid === sid);\n // Alchemy keeps the last binding with a given sid. Reject conflicting owners before that happens.\n if (previous !== undefined) {\n const existing = Schema.decodeUnknownSync(Schema.Struct({ path: Schema.String }))(\n previous.data,\n );\n if (existing.path !== options.component) {\n return yield* Effect.die(\n new Error(\n `Alchemy namespace ${JSON.stringify(namespace)} is already bound to ${existing.path}.`,\n ),\n );\n }\n return;\n }\n yield* options.deployment.bind(sid, {\n type: \"component\",\n path: options.component,\n namespace,\n });\n });\n","// Convex.SharedHubClient: one deployment of an app as a client of the central identity hub, for an\n// app that deploys in its own GitHub Actions run, on any host.\n//\n// The run asks samebase.com for a new client with a GitHub OIDC token, so the stack needs no\n// Samebase key. The token names the repository. Samebase reads `/samebase-verification.json` at the\n// origin of the return address, which must name the same repository, and registers the client on\n// that address. The answer holds the client's own registration access token (RFC 7591), and each\n// later deploy reads the client on the hub with that token (RFC 7592). The hub keeps only hashes, so\n// the state is the only record of the secret and the token.\n//\n// A push removes a variable that a deploy stops passing, so a failure here must not drop working\n// variables or stop the deploy. A renewal that fails keeps the saved values. Only a client that\n// cannot work asks for a new one: it is gone, or it has another return address. If that request\n// fails, the run gives the off values, so the app has guest sign-in and not a broken button.\n// The off values are the `no-hub` variables of a checkout without a hub: Convex needs the variables,\n// and the app hides the button for this client ID.\n//\n// No text in this file quotes an answer or a fetch error, so no log line can hold a secret.\nimport { Resource } from \"alchemy\";\nimport { isResolved } from \"alchemy/Diff\";\nimport * as Provider from \"alchemy/Provider\";\nimport * as Effect from \"effect/Effect\";\nimport * as Option from \"effect/Option\";\nimport * as Redacted from \"effect/Redacted\";\nimport * as Schema from \"effect/Schema\";\nimport type { Providers } from \"./Providers.ts\";\n\n/** A hung service must not hold a deploy: the time limit of one request. */\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nexport interface SharedHubClientProps {\n /** The stable name of the app, set once in the stack. */\n readonly app: string;\n /**\n * The return address of the hub's sign-ins, such as `${site.siteUrl}/auth/hub/callback`: an https\n * URL whose origin serves `/samebase-verification.json` with the numeric GitHub id of this\n * repository, `{ \"repository\": \"123\" }`. It can come from a resource that this run creates.\n */\n readonly redirectUri: string;\n readonly deploymentType: \"production\" | \"preview\";\n /** Recorded on the hub's client, such as the commit. The hub records \"unknown\" without it. */\n readonly softwareVersion?: string;\n}\n\nexport interface SharedHubClientAttributes {\n /** `off`: no client, and the values below are the off values. */\n readonly status: \"on\" | \"off\" | \"suspended\";\n /** The issuer of the hub, which the deployment names to sign in. */\n readonly issuer: string;\n readonly clientId: string;\n /** Returned only at registration. */\n readonly clientSecret: Redacted.Redacted<string>;\n /** Returned only at registration. It reads and deletes this client on the hub, and nothing else. */\n readonly registrationAccessToken: Redacted.Redacted<string>;\n readonly registrationClientUri: string;\n readonly deploymentType: \"production\" | \"preview\";\n /** The return address that the client serves. Empty for the off values. */\n readonly redirectUri: string;\n}\n\nexport type SharedHubClient = Resource<\n \"IdentityHub.SharedClient\",\n SharedHubClientProps,\n SharedHubClientAttributes,\n never,\n Providers\n>;\nexport const SharedHubClient = Resource<SharedHubClient>(\"IdentityHub.SharedClient\");\n\n/** One request gave no usable answer. The reason holds no secret. */\nclass SharedHubClientFailed extends Schema.TaggedError<SharedHubClientFailed>()(\n \"SharedHubClientFailed\",\n { reason: Schema.String },\n) {}\n\nconst OidcToken = Schema.Struct({ value: Schema.NonEmptyString });\n\n/** The 201 answer of `POST /api/hub-clients`. */\nconst NewClient = Schema.Struct({\n issuer: Schema.NonEmptyString,\n client_id: Schema.NonEmptyString,\n client_secret: Schema.NonEmptyString,\n registration_access_token: Schema.NonEmptyString,\n registration_client_uri: Schema.NonEmptyString,\n redirect_uri: Schema.NonEmptyString,\n});\n\n/** The other answers of `POST /api/hub-clients`, by status. */\nconst REFUSALS: Readonly<Record<number, string>> = {\n 400: \"Samebase refused the request: the return address must be https on a DNS name\",\n 401: \"Samebase refused the GitHub OIDC token\",\n 403: \"Samebase does not allow this repository or return address\",\n 404: \"hub clients are off on Samebase\",\n 429: \"Samebase limits the requests of this repository\",\n 502: \"the identity hub is not available\",\n};\n\n/** The reasons of a 403 that a log line shows. Any other text of the answer is never logged. */\nconst NOT_ALLOWED = new Map([\n [\"run_not_allowed\", \"Samebase gives clients only to runs of main and of pull requests\"],\n [\"repository_not_connected\", \"the repository is not connected to Samebase\"],\n [\"owner_mismatch\", \"the repository's owner is not the owner connected to Samebase\"],\n [\"host_unreachable\", \"Samebase could not reach the host of the return address\"],\n [\n \"verification_file_missing\",\n \"the host of the return address does not serve /samebase-verification.json\",\n ],\n [\"verification_file_invalid\", \"/samebase-verification.json on the host holds no repository id\"],\n [\"repository_mismatch\", \"/samebase-verification.json on the host names another repository\"],\n [\"hub_refused\", \"the identity hub refused this repository\"],\n]);\n\nconst Refusal = Schema.Struct({ reason: Schema.String });\n\n/** The text of a refusal: a known reason of a 403, else the text of its status. */\nconst refusalOf = (status: number, body: unknown) => {\n const reason =\n status === 403\n ? Option.getOrUndefined(Schema.decodeUnknownOption(Refusal)(body))?.reason\n : undefined;\n return (\n (reason === undefined ? undefined : NOT_ALLOWED.get(reason)) ??\n REFUSALS[status] ??\n \"Samebase refused the request\"\n );\n};\n\nconst send = (who: string, url: string, token: string, init: { method: string; body?: string }) =>\n Effect.tryPromise({\n try: async () => {\n const response = await fetch(url, {\n ...init,\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n const body: unknown = await response.json().catch(() => undefined);\n return { status: response.status, body };\n },\n catch: () => new SharedHubClientFailed({ reason: `${who} gave no answer` }),\n });\n\nconst decode =\n <S extends Schema.Codec<unknown, unknown>>(schema: S, who: string) =>\n (value: unknown) =>\n Schema.decodeUnknownEffect(schema)(value).pipe(\n Effect.mapError(() => new SharedHubClientFailed({ reason: `${who} gave a bad answer` })),\n );\n\n/** The OIDC token of this run, read right before the call, because it lives for minutes only. */\nconst readOidcToken = (requestUrl: string, requestToken: string) =>\n Effect.gen(function* () {\n const { body, status } = yield* send(\n \"GitHub\",\n `${requestUrl}&audience=samebase`,\n requestToken,\n { method: \"GET\" },\n );\n if (status !== 200) {\n return yield* new SharedHubClientFailed({\n reason: `GitHub gave no OIDC token (HTTP ${status})`,\n });\n }\n return (yield* decode(OidcToken, \"GitHub\")(body)).value;\n });\n\nconst requestNewClient = (props: SharedHubClientProps, requestUrl: string, requestToken: string) =>\n Effect.gen(function* () {\n const token = yield* readOidcToken(requestUrl, requestToken);\n const { body, status } = yield* send(\n \"Samebase\",\n `${process.env[\"SAMEBASE_URL\"] ?? \"https://samebase.com\"}/api/hub-clients`,\n token,\n {\n method: \"POST\",\n body: JSON.stringify({\n app: props.app,\n redirectUri: props.redirectUri,\n softwareVersion: props.softwareVersion ?? \"unknown\",\n }),\n },\n );\n if (status !== 201) {\n return yield* new SharedHubClientFailed({\n reason: `${refusalOf(status, body)} (HTTP ${status})`,\n });\n }\n const client = yield* decode(NewClient, \"Samebase\")(body);\n return {\n status: \"on\" as const,\n issuer: client.issuer,\n clientId: client.client_id,\n clientSecret: Redacted.make(client.client_secret),\n registrationAccessToken: Redacted.make(client.registration_access_token),\n registrationClientUri: client.registration_client_uri,\n deploymentType: props.deploymentType,\n redirectUri: client.redirect_uri,\n };\n });\n\n/** A new client, or `saved`, or the off values. `saved` holds no off values. */\nconst newClient = (props: SharedHubClientProps) => {\n const label = `Convex.SharedHubClient: ${props.app} on ${props.redirectUri} deploys without the hub`;\n const off: SharedHubClientAttributes = {\n status: \"off\",\n issuer: \"http://127.0.0.1:1\",\n clientId: \"no-hub\",\n clientSecret: Redacted.make(\"no-hub\"),\n registrationAccessToken: Redacted.make(\"\"),\n registrationClientUri: \"\",\n deploymentType: props.deploymentType,\n redirectUri: \"\",\n };\n const requestUrl = process.env[\"ACTIONS_ID_TOKEN_REQUEST_URL\"];\n const requestToken = process.env[\"ACTIONS_ID_TOKEN_REQUEST_TOKEN\"];\n // A laptop, or a repository outside Samebase. In a workflow, `id-token: write` gives both, so a\n // workflow without them is a mistake to show, not a normal run.\n if (!requestUrl || !requestToken) {\n return (\n process.env[\"GITHUB_ACTIONS\"] === \"true\"\n ? Effect.logWarning(`${label}: this workflow needs the permission id-token: write.`)\n : Effect.logInfo(`${label}: this run has no GitHub OIDC token.`)\n ).pipe(Effect.as(off));\n }\n return requestNewClient(props, requestUrl, requestToken).pipe(\n Effect.catchTag(\"SharedHubClientFailed\", (error) =>\n Effect.logWarning(`${label}: ${error.reason}. The next deploy asks again.`).pipe(\n Effect.as(off),\n ),\n ),\n );\n};\n\n/** The client after a read on the hub, or undefined when the hub says that the client is gone. */\nconst renew = (saved: SharedHubClientAttributes) =>\n Effect.gen(function* () {\n const { status } = yield* send(\n \"The identity hub\",\n saved.registrationClientUri,\n Redacted.value(saved.registrationAccessToken),\n { method: \"GET\" },\n );\n switch (status) {\n case 200:\n return { ...saved, status: \"on\" as const };\n case 401:\n return undefined;\n case 403:\n yield* Effect.logWarning(\n `Convex.SharedHubClient: the hub suspended the client of ${saved.redirectUri}. It keeps its values, and nobody can sign in with it.`,\n );\n return { ...saved, status: \"suspended\" as const };\n default:\n return yield* new SharedHubClientFailed({\n reason: `The identity hub answered HTTP ${status}`,\n });\n }\n }).pipe(\n Effect.catchTag(\"SharedHubClientFailed\", (error) =>\n Effect.logWarning(\n `Convex.SharedHubClient: ${saved.redirectUri} keeps the saved client: ${error.reason}.`,\n ).pipe(Effect.as(saved)),\n ),\n );\n\nexport const SharedHubClientProvider = () =>\n Provider.succeed(SharedHubClient, {\n // Always an update, so that each deploy reaches the hub. No attribute is stable, so a push\n // reads the values that this run ended with. The same values cause no other push.\n diff: ({ news }) =>\n Effect.succeed(isResolved(news) ? ({ action: \"update\" } as const) : undefined),\n\n read: ({ output }) => Effect.succeed(output),\n\n reconcile: Effect.fn(function* ({ news, output }) {\n const saved = output?.status === \"off\" ? undefined : output;\n const renewed = saved?.redirectUri === news.redirectUri ? yield* renew(saved) : undefined;\n return renewed ?? (yield* newClient(news));\n }),\n\n // A failed delete does not stop a destroy: the removal date of a preview client cleans it up.\n // A destroy can leave a production deployment running, so its client stays.\n delete: ({ output }) =>\n output.deploymentType === \"production\" || output.status === \"off\"\n ? Effect.void\n : Effect.gen(function* () {\n const { status } = yield* send(\n \"The identity hub\",\n output.registrationClientUri,\n Redacted.value(output.registrationAccessToken),\n { method: \"DELETE\" },\n );\n // 401: the client is gone already.\n if (status !== 204 && status !== 401) {\n return yield* new SharedHubClientFailed({\n reason: `The identity hub answered HTTP ${status}`,\n });\n }\n }).pipe(\n Effect.catchTag(\"SharedHubClientFailed\", (error) =>\n Effect.logWarning(\n `Convex.SharedHubClient: the client of ${output.redirectUri} stays on the hub until its removal date: ${error.reason}.`,\n ),\n ),\n ),\n });\n","// The provider collection users merge into a stack:\n// providers: Layer.mergeAll(Cloudflare.providers(), Convex.providers())\nimport * as Provider from \"alchemy/Provider\";\nimport * as Layer from \"effect/Layer\";\nimport { type Credentials, fromConfig } from \"./Credentials.ts\";\nimport { DeploymentProvider, DeploymentResource } from \"./Deployment.ts\";\nimport { HubClient, HubClientProvider } from \"./HubClient.ts\";\nimport { ManagementApiLive } from \"./ManagementApi.ts\";\nimport { Project, ProjectProvider } from \"./Project.ts\";\nimport { SharedHubClient, SharedHubClientProvider } from \"./SharedHubClient.ts\";\n\nexport class Providers extends Provider.ProviderCollection<Providers>()(\"Convex\") {}\n\n/**\n * The Convex resource providers. Pass a credentials layer to override the\n * default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login, and\n * `CONVEX_DEPLOY_KEY`).\n */\nexport const providers = (credentials: Layer.Layer<Credentials> = fromConfig()) =>\n Layer.effect(\n Providers,\n Provider.collection([Project, DeploymentResource, HubClient, SharedHubClient]),\n ).pipe(\n Layer.provide(\n Layer.mergeAll(\n ProjectProvider(),\n DeploymentProvider(),\n HubClientProvider(),\n SharedHubClientProvider(),\n ),\n ),\n Layer.provide(ManagementApiLive()),\n Layer.provide(credentials),\n Layer.orDie,\n );\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/Component.ts","../src/SharedHubClient.ts","../src/Providers.ts"],"sourcesContent":["import { toPath } from \"alchemy/FQN\";\nimport type { NamespaceNode } from \"alchemy/Namespace\";\nimport { Stack } from \"alchemy/Stack\";\nimport * as Effect from \"effect/Effect\";\nimport * as Schema from \"effect/Schema\";\nimport type { Deployment } from \"./Deployment.ts\";\n\n/** A component's resources occupy this namespace in the deployment's Alchemy stack and stage. */\nexport interface ComponentBinding {\n readonly type: \"component\";\n readonly path: string;\n readonly namespace: readonly string[];\n}\n\n/**\n * Associate a native namespace with an installed component. `component` is the\n * installed name from the app's `convex.config.ts`; a nested install joins the\n * names with slashes, such as `\"workspace/mail\"`. An undefined namespace\n * associates the stack root. This records display metadata; it does not install\n * the component, set its environment, or change removal policies.\n */\nexport const bindComponent = (options: {\n deployment: Deployment;\n component: string;\n namespace: NamespaceNode | undefined;\n}) =>\n Effect.gen(function* () {\n if (options.component === \"\") {\n return yield* Effect.die(new Error(\"The component name is empty.\"));\n }\n const namespace = toPath(options.namespace);\n const sid = `convex.component:${JSON.stringify(namespace)}`;\n const stack = yield* Stack;\n const previous = stack.bindings[options.deployment.FQN]?.find((binding) => binding.sid === sid);\n // Alchemy keeps the last binding with a given sid. Reject conflicting owners before that happens.\n if (previous !== undefined) {\n const existing = Schema.decodeUnknownSync(Schema.Struct({ path: Schema.String }))(\n previous.data,\n );\n if (existing.path !== options.component) {\n return yield* Effect.die(\n new Error(\n `Alchemy namespace ${JSON.stringify(namespace)} is already bound to ${existing.path}.`,\n ),\n );\n }\n return;\n }\n yield* options.deployment.bind(sid, {\n type: \"component\",\n path: options.component,\n namespace,\n });\n });\n","// Convex.SharedHubClient: one deployment of an app as a client of the central identity hub, for an\n// app that deploys in its own GitHub Actions run, on any host.\n//\n// The run asks the hub for a new client with a GitHub OIDC token, so the stack needs no key and no\n// Samebase account. The token names the repository. The hub reads `/samebase-verification.json` at\n// the origin of the return address, which must name the same repository, and registers the client on\n// that address. The answer holds the client's own registration access token (RFC 7591), and each\n// later deploy reads the client on the hub with that token (RFC 7592). The hub keeps only hashes, so\n// the state is the only record of the secret and the token.\n//\n// A push removes a variable that a deploy stops passing, so a failure here must not drop working\n// variables or stop the deploy. A renewal that fails keeps the saved values. Only a client that\n// cannot work asks for a new one: it is gone, or it has another return address. If that request\n// fails, the run gives the off values, so the app has guest sign-in and not a broken button.\n// The off values are the `no-hub` variables of a checkout without a hub: Convex needs the variables,\n// and the app hides the button for this client ID.\n//\n// No text in this file quotes an answer or a fetch error, so no log line can hold a secret.\nimport { Resource } from \"alchemy\";\nimport { isResolved } from \"alchemy/Diff\";\nimport * as Provider from \"alchemy/Provider\";\nimport * as Effect from \"effect/Effect\";\nimport * as Option from \"effect/Option\";\nimport * as Redacted from \"effect/Redacted\";\nimport * as Schema from \"effect/Schema\";\nimport type { Providers } from \"./Providers.ts\";\n\n/** A hung service must not hold a deploy: the time limit of one request. */\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nexport interface SharedHubClientProps {\n /** The stable name of the app, set once in the stack. */\n readonly app: string;\n /**\n * The return address of the hub's sign-ins, such as `${site.siteUrl}/auth/hub/callback`: an https\n * URL whose origin serves `/samebase-verification.json` with the numeric GitHub id of this\n * repository, `{ \"repository\": \"123\" }`. It can come from a resource that this run creates.\n */\n readonly redirectUri: string;\n readonly deploymentType: \"production\" | \"preview\";\n /** Recorded on the hub's client, such as the commit. The hub records \"unknown\" without it. */\n readonly softwareVersion?: string;\n}\n\nexport interface SharedHubClientAttributes {\n /** `off`: no client, and the values below are the off values. */\n readonly status: \"on\" | \"off\" | \"suspended\";\n /** The issuer of the hub, which the deployment names to sign in. */\n readonly issuer: string;\n readonly clientId: string;\n /** Returned only at registration. */\n readonly clientSecret: Redacted.Redacted<string>;\n /** Returned only at registration. It reads and deletes this client on the hub, and nothing else. */\n readonly registrationAccessToken: Redacted.Redacted<string>;\n readonly registrationClientUri: string;\n readonly deploymentType: \"production\" | \"preview\";\n /** The return address that the client serves. Empty for the off values. */\n readonly redirectUri: string;\n}\n\nexport type SharedHubClient = Resource<\n \"IdentityHub.SharedClient\",\n SharedHubClientProps,\n SharedHubClientAttributes,\n never,\n Providers\n>;\nexport const SharedHubClient = Resource<SharedHubClient>(\"IdentityHub.SharedClient\");\n\n/** One request gave no usable answer. The reason holds no secret. */\nclass SharedHubClientFailed extends Schema.TaggedError<SharedHubClientFailed>()(\n \"SharedHubClientFailed\",\n { reason: Schema.String },\n) {}\n\nconst OidcToken = Schema.Struct({ value: Schema.NonEmptyString });\n\n/** The 201 answer of `POST /hub-clients`. */\nconst NewClient = Schema.Struct({\n issuer: Schema.NonEmptyString,\n client_id: Schema.NonEmptyString,\n client_secret: Schema.NonEmptyString,\n registration_access_token: Schema.NonEmptyString,\n registration_client_uri: Schema.NonEmptyString,\n redirect_uri: Schema.NonEmptyString,\n /** The terms of the shared sign-in service, which the log shows once, for the new client. */\n terms: Schema.NonEmptyString,\n});\n\n/** The other answers of `POST /hub-clients`, by status. */\nconst REFUSALS: Readonly<Record<number, string>> = {\n 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\",\n 401: \"the identity hub refused the GitHub OIDC token\",\n 403: \"the identity hub does not allow this repository or return address\",\n 404: \"the identity hub has no /hub-clients\",\n 429: \"the identity hub limits new clients\",\n 502: \"GitHub's keys are not available to the identity hub\",\n};\n\n/** The reasons of a 403 that a log line shows. Any other text of the answer is never logged. */\nconst NOT_ALLOWED = new Map([\n [\"paused\", \"the identity hub pauses new clients of third-party apps\"],\n [\"run_not_allowed\", \"the identity hub gives clients only to runs of main and of pull requests\"],\n [\"blocked\", \"Samebase blocked this repository or its owner\"],\n [\"suspended\", \"the identity hub suspended this repository\"],\n [\"too_many_apps\", \"the repository has as many apps on the identity hub as it may\"],\n [\"too_many_clients\", \"the app has as many clients on the identity hub as it may\"],\n [\"app_taken\", \"the app's name belongs to another app on the identity hub\"],\n [\"host_unreachable\", \"the identity hub could not reach the host of the return address\"],\n [\n \"verification_file_missing\",\n \"the host of the return address does not serve /samebase-verification.json\",\n ],\n [\"verification_file_invalid\", \"/samebase-verification.json on the host holds no repository id\"],\n [\"repository_mismatch\", \"/samebase-verification.json on the host names another repository\"],\n]);\n\n/** The windows of a 429, by the name that the answer gives. */\nconst RATE_LIMITED = new Map([\n [\"repository\", \"the identity hub gives one repository at most 30 new clients an hour\"],\n [\"owner\", \"the identity hub gives one owner at most 100 new clients an hour\"],\n [\"new_owners\", \"the identity hub takes at most 50 new owners an hour\"],\n]);\n\nconst Refusal = Schema.Struct({ reason: Schema.String });\nconst RateLimited = Schema.Struct({ window: Schema.String });\n\n/** The text of a refusal: a known reason of a 403 or window of a 429, else the text of its status. */\nconst refusalOf = (status: number, body: unknown) => {\n const known =\n status === 403\n ? NOT_ALLOWED.get(\n Option.getOrUndefined(Schema.decodeUnknownOption(Refusal)(body))?.reason ?? \"\",\n )\n : status === 429\n ? RATE_LIMITED.get(\n Option.getOrUndefined(Schema.decodeUnknownOption(RateLimited)(body))?.window ?? \"\",\n )\n : undefined;\n return known ?? REFUSALS[status] ?? \"the identity hub refused the request\";\n};\n\nconst send = (who: string, url: string, token: string, init: { method: string; body?: string }) =>\n Effect.tryPromise({\n try: async () => {\n const response = await fetch(url, {\n ...init,\n headers: { Authorization: `Bearer ${token}`, \"Content-Type\": \"application/json\" },\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n const body: unknown = await response.json().catch(() => undefined);\n return { status: response.status, body };\n },\n catch: () => new SharedHubClientFailed({ reason: `${who} gave no answer` }),\n });\n\nconst decode =\n <S extends Schema.Codec<unknown, unknown>>(schema: S, who: string) =>\n (value: unknown) =>\n Schema.decodeUnknownEffect(schema)(value).pipe(\n Effect.mapError(() => new SharedHubClientFailed({ reason: `${who} gave a bad answer` })),\n );\n\n/** The OIDC token of this run, read right before the call, because it lives for minutes only. */\nconst readOidcToken = (requestUrl: string, requestToken: string) =>\n Effect.gen(function* () {\n const { body, status } = yield* send(\n \"GitHub\",\n `${requestUrl}&audience=samebase`,\n requestToken,\n { method: \"GET\" },\n );\n if (status !== 200) {\n return yield* new SharedHubClientFailed({\n reason: `GitHub gave no OIDC token (HTTP ${status})`,\n });\n }\n return (yield* decode(OidcToken, \"GitHub\")(body)).value;\n });\n\nconst requestNewClient = (props: SharedHubClientProps, requestUrl: string, requestToken: string) =>\n Effect.gen(function* () {\n const token = yield* readOidcToken(requestUrl, requestToken);\n const { body, status } = yield* send(\n \"The identity hub\",\n `${process.env[\"HUB_URL\"] ?? \"https://auth.samebase.com\"}/hub-clients`,\n token,\n {\n method: \"POST\",\n body: JSON.stringify({\n app: props.app,\n redirectUri: props.redirectUri,\n softwareVersion: props.softwareVersion ?? \"unknown\",\n }),\n },\n );\n if (status !== 201) {\n return yield* new SharedHubClientFailed({\n reason: `${refusalOf(status, body)} (HTTP ${status})`,\n });\n }\n const client = yield* decode(NewClient, \"The identity hub\")(body);\n yield* Effect.logInfo(\n `Convex.SharedHubClient: ${props.app} on ${client.redirect_uri} has a new client of the identity hub. Its use is under ${client.terms}.`,\n );\n return {\n status: \"on\" as const,\n issuer: client.issuer,\n clientId: client.client_id,\n clientSecret: Redacted.make(client.client_secret),\n registrationAccessToken: Redacted.make(client.registration_access_token),\n registrationClientUri: client.registration_client_uri,\n deploymentType: props.deploymentType,\n redirectUri: client.redirect_uri,\n };\n });\n\n/** A new client, or `saved`, or the off values. `saved` holds no off values. */\nconst newClient = (props: SharedHubClientProps) => {\n const label = `Convex.SharedHubClient: ${props.app} on ${props.redirectUri} deploys without the hub`;\n const off: SharedHubClientAttributes = {\n status: \"off\",\n issuer: \"http://127.0.0.1:1\",\n clientId: \"no-hub\",\n clientSecret: Redacted.make(\"no-hub\"),\n registrationAccessToken: Redacted.make(\"\"),\n registrationClientUri: \"\",\n deploymentType: props.deploymentType,\n redirectUri: \"\",\n };\n const requestUrl = process.env[\"ACTIONS_ID_TOKEN_REQUEST_URL\"];\n const requestToken = process.env[\"ACTIONS_ID_TOKEN_REQUEST_TOKEN\"];\n // A laptop, or another CI. In a workflow, `id-token: write` gives both, so a workflow without them\n // is a mistake to show, not a normal run.\n if (!requestUrl || !requestToken) {\n return (\n process.env[\"GITHUB_ACTIONS\"] === \"true\"\n ? Effect.logWarning(`${label}: this workflow needs the permission id-token: write.`)\n : Effect.logInfo(`${label}: this run has no GitHub OIDC token.`)\n ).pipe(Effect.as(off));\n }\n return requestNewClient(props, requestUrl, requestToken).pipe(\n Effect.catchTag(\"SharedHubClientFailed\", (error) =>\n Effect.logWarning(`${label}: ${error.reason}. The next deploy asks again.`).pipe(\n Effect.as(off),\n ),\n ),\n );\n};\n\n/** The client after a read on the hub, or undefined when the hub says that the client is gone. */\nconst renew = (saved: SharedHubClientAttributes) =>\n Effect.gen(function* () {\n const { status } = yield* send(\n \"The identity hub\",\n saved.registrationClientUri,\n Redacted.value(saved.registrationAccessToken),\n { method: \"GET\" },\n );\n switch (status) {\n case 200:\n return { ...saved, status: \"on\" as const };\n case 401:\n return undefined;\n case 403:\n yield* Effect.logWarning(\n `Convex.SharedHubClient: the hub suspended the client of ${saved.redirectUri}. It keeps its values, and nobody can sign in with it.`,\n );\n return { ...saved, status: \"suspended\" as const };\n default:\n return yield* new SharedHubClientFailed({\n reason: `The identity hub answered HTTP ${status}`,\n });\n }\n }).pipe(\n Effect.catchTag(\"SharedHubClientFailed\", (error) =>\n Effect.logWarning(\n `Convex.SharedHubClient: ${saved.redirectUri} keeps the saved client: ${error.reason}.`,\n ).pipe(Effect.as(saved)),\n ),\n );\n\nexport const SharedHubClientProvider = () =>\n Provider.succeed(SharedHubClient, {\n // Always an update, so that each deploy reaches the hub. No attribute is stable, so a push\n // reads the values that this run ended with. The same values cause no other push.\n diff: ({ news }) =>\n Effect.succeed(isResolved(news) ? ({ action: \"update\" } as const) : undefined),\n\n read: ({ output }) => Effect.succeed(output),\n\n reconcile: Effect.fn(function* ({ news, output }) {\n const saved = output?.status === \"off\" ? undefined : output;\n const renewed = saved?.redirectUri === news.redirectUri ? yield* renew(saved) : undefined;\n return renewed ?? (yield* newClient(news));\n }),\n\n // A failed delete does not stop a destroy: the removal date of a preview client cleans it up.\n // A destroy can leave a production deployment running, so its client stays.\n delete: ({ output }) =>\n output.deploymentType === \"production\" || output.status === \"off\"\n ? Effect.void\n : Effect.gen(function* () {\n const { status } = yield* send(\n \"The identity hub\",\n output.registrationClientUri,\n Redacted.value(output.registrationAccessToken),\n { method: \"DELETE\" },\n );\n // 401: the client is gone already.\n if (status !== 204 && status !== 401) {\n return yield* new SharedHubClientFailed({\n reason: `The identity hub answered HTTP ${status}`,\n });\n }\n }).pipe(\n Effect.catchTag(\"SharedHubClientFailed\", (error) =>\n Effect.logWarning(\n `Convex.SharedHubClient: the client of ${output.redirectUri} stays on the hub until its removal date: ${error.reason}.`,\n ),\n ),\n ),\n });\n","// The provider collection users merge into a stack:\n// providers: Layer.mergeAll(Cloudflare.providers(), Convex.providers())\nimport * as Provider from \"alchemy/Provider\";\nimport * as Layer from \"effect/Layer\";\nimport { type Credentials, fromConfig } from \"./Credentials.ts\";\nimport { DeploymentProvider, DeploymentResource } from \"./Deployment.ts\";\nimport { HubClient, HubClientProvider } from \"./HubClient.ts\";\nimport { ManagementApiLive } from \"./ManagementApi.ts\";\nimport { Project, ProjectProvider } from \"./Project.ts\";\nimport { SharedHubClient, SharedHubClientProvider } from \"./SharedHubClient.ts\";\n\nexport class Providers extends Provider.ProviderCollection<Providers>()(\"Convex\") {}\n\n/**\n * The Convex resource providers. Pass a credentials layer to override the\n * default lookup (`CONVEX_ACCESS_TOKEN`, then the Convex CLI login, and\n * `CONVEX_DEPLOY_KEY`).\n */\nexport const providers = (credentials: Layer.Layer<Credentials> = fromConfig()) =>\n Layer.effect(\n Providers,\n Provider.collection([Project, DeploymentResource, HubClient, SharedHubClient]),\n ).pipe(\n Layer.provide(\n Layer.mergeAll(\n ProjectProvider(),\n DeploymentProvider(),\n HubClientProvider(),\n SharedHubClientProvider(),\n ),\n ),\n Layer.provide(ManagementApiLive()),\n Layer.provide(credentials),\n Layer.orDie,\n );\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samebase/alchemy-convex",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "The Convex plane for Alchemy v2: projects, deployments, environment variables, and the push of the functions",
5
5
  "keywords": [
6
6
  "alchemy",
@@ -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"