a2a-exposed 0.7.34 → 0.8.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/lib/account.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // Control-plane sign-in (RFC 8628). The CLI is a public client (`a2a-cli`).
1
+ // Device-flow sign-in (RFC 8628). The CLI is a public client (`a2a-cli`).
2
2
  // The session token is written to config.env and is never printed.
3
3
  import * as C from "./config.mjs";
4
4
  import { CLI, die } from "./a2a.mjs";
@@ -6,7 +6,7 @@ import { CLI, die } from "./a2a.mjs";
6
6
  export const CLI_CLIENT_ID = "a2a-cli";
7
7
  export const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
8
8
 
9
- /** https origin with no userinfo, or "" when the value cannot be a control plane. */
9
+ /** https origin with no userinfo, or "" when the value has a path, query, or userinfo. */
10
10
  export function controlOrigin(value) {
11
11
  const raw = String(value || "").trim();
12
12
  if (!raw) return "";
package/lib/feedback.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // Feedback: post to a hosted control plane when this inbox points at one, otherwise print a GitHub issue link.
1
+ // Feedback: post when this inbox names a feedback URL, otherwise print a GitHub issue link.
2
2
  import os from "node:os";
3
3
  import readline from "node:readline/promises";
4
4
  import { CliError } from "./a2a.mjs";
@@ -29,7 +29,7 @@ export function scrubFeedbackText(value) {
29
29
 
30
30
  const REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
31
31
 
32
- /** The repository that will receive the issue. The prefilled form is ISSUE_REPO. A hosted post uses the control plane's name. */
32
+ /** The repository that will receive the issue. The prefilled form is ISSUE_REPO. A hosted post uses the name that URL returns. */
33
33
  export function publicIssueWarning(repo = ISSUE_REPO) {
34
34
  const name = typeof repo === "string" && REPO_RE.test(repo) && repo.length <= 100 && !repo.includes("..") ? repo : ISSUE_REPO;
35
35
  return `This report is filed as a public GitHub issue on ${name}. Do not include confidential information: secrets, tokens, owner or pairing tokens, private URLs or hostnames, message contents, or personal data.`;
@@ -233,7 +233,7 @@ export async function feedbackPublicRepo(input) {
233
233
  }
234
234
  const body = named && named.ok ? await readJson(named) : null;
235
235
  const repo = namedRepo(body?.repo);
236
- if (!repo) throw new CliError("the control plane did not name the public repository; feedback was not sent");
236
+ if (!repo) throw new CliError("the inbox did not name the public repository; feedback was not sent");
237
237
  return repo;
238
238
  }
239
239
 
package/lib/main.mjs CHANGED
@@ -172,41 +172,11 @@ Outbound (agents you call)
172
172
  poll --to <alias|url> <taskId> [--proto 0.3|1.0]
173
173
  outbound <taskId> stored state of a task you sent (incl. pushed updates)
174
174
 
175
- Control plane account (the hosted dashboard is https://a2a.exposed)
176
- signup --control-url https://control.example
177
- login --control-url https://control.example
178
- device-flow sign-in. Prints a code and a page. Type that code
179
- on the page; the link alone does not approve. The session is saved
180
- as CONTROL_TOKEN in config.env and is not printed. signup and login
181
- are the same flow; signup is how the first account is created.
182
- --json prints the control origin and email, not the token.
183
- A browser sign-in that does not complete returns to /app?error=<code>.
184
- The page allows each enabled provider origin in form-action, so the
185
- browser can follow the redirect. Buttons say Continue with that
186
- provider. Until the check finishes they stay muted and the page says
187
- Waiting for the check…. A reused token says The check expired. Please try again.
188
- After sign-in, /app shows the account and its agents. Each agent is listed
189
- by its hostname. A hosted name opens
190
- that agent's page: MCP connectors (revoke needs a recent sign-in), a
191
- message log of recent history. Opening this log leaves a pending wake in place until you list the inbox. The SQL download
192
- is a redacted copy and replaces a pasted owner token. Peer times are labeled UTC. A pairing
193
- token shows its code. An empty outbound list says peers added with the CLI
194
- on your machine stay in that CLI. Sign out is on that
195
- page. Creating an agent shows the owner token once. The agent page rotates
196
- that token after a confirmation and a recent sign-in. The new token is
197
- shown once on that agent's page. The previous one stops working immediately.
198
- The token is shown only when the stored hash is still that token. A Copy button sits next to it. Without JavaScript the value still copies.
199
- Deleting an agent asks for its name and a recent sign-in. The account page says how many hosted slots remain. A self-hosted inbox has no such delete.
200
- That page is not stored.
201
- FREE_AGENT_LIMIT=2 caps a free account at 2 hosted agents. A Cloudflare agent stays outside that count. An account's own limit overrides it.
202
- Unset, there is no limit. The database rejects another hosted insert.
203
- SIGNUP_ALLOWLIST, when set, admits only those verified emails. Unset, sign-in stays open.
204
- Sign-in uses GitHub, Google, or email. Connect Cloudflare from the account page after you sign in.
205
- A refusal returns to /app?error=invite and the page says Sign-in is invite-only for now.
206
- The control Worker logs social_sign_in_failed before the
207
- provider page, social_sign_in_timeout when that sign-in does not
208
- return, and oauth_callback_failed when the provider sends the browser
209
- back
175
+ Owner
176
+ One inbox. The owner token from init authenticates inbox, reply, export, and the other owner
177
+ commands. GET / in a browser shows the agent card. /device approves or denies a pairing request.
178
+ pair set-password sets the approval password. pair set-oidc adds OpenID Connect on /device and
179
+ the MCP consent page when the issuer, client id, client secret, and allowlist are all set.
210
180
 
211
181
  Feedback
212
182
  feedback "message" [--kind bug|idea|other] [--contact x]
@@ -215,15 +185,15 @@ Feedback
215
185
  [--connection webhook|proxy|polling] [--yes]
216
186
  report a bug or an idea. A bug needs --steps.
217
187
  The report is a public GitHub issue. The warning names
218
- the repository before it sends. A hosted inbox uses the
219
- control plane's FEEDBACK_REPO. Otherwise the link opens
188
+ the repository before it sends. A hosted inbox names
189
+ that repository. Otherwise the link opens
220
190
  the form on telegraphic-dev/a2a-exposed. Do not include
221
191
  confidential information: secrets, tokens, owner or pairing
222
192
  tokens, private URLs or hostnames, message contents, or
223
193
  personal data. Prints that warning and asks before sending,
224
194
  unless --yes is set. Off a terminal, --yes is required.
225
- Posts to the control plane when this inbox is hosted and that
226
- plane accepts feedback. Otherwise prints a link to the bug
195
+ Posts when this inbox names a feedback URL.
196
+ Otherwise prints a link to the bug
227
197
  report form and does not open a browser. The CLI adds its
228
198
  version, the inbox Worker version when the inbox reports it,
229
199
  the OS, the Node version, the connection (webhook, proxy, or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2a-exposed",
3
- "version": "0.7.34",
3
+ "version": "0.8.0",
4
4
  "description": "Give any AI agent a public A2A (Agent2Agent) endpoint: Cloudflare Worker inbox + wake webhook or scheduled inbox checks",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,10 +46,7 @@ const plain: Record<string, string> = {
46
46
  // Access (never a Tailnet / LAN URL: the Worker can't reach those, and they never appear in the public card)
47
47
  UPSTREAM_URL: v("A2A_UPSTREAM_URL"),
48
48
  UPSTREAM_CARD_URL: v("A2A_UPSTREAM_CARD_URL"), // default <upstream origin>/.well-known/agent-card.json
49
- // Hosted multi-tenant gates. All empty by default, and empty is omitted, so a self-host deploy is unchanged.
50
- // TENANCY=host switches the entrypoint to src/hosted.ts and adds TENANT_DO + TENANT_DIRECTORY below.
51
- // QUOTAS, USAGE_SINK, SIGNUP_URL and BRANDING are parsed and reserved.
52
- // FEEDBACK_URL is an https origin. Only a hosted tenant posts feedback there. Unset on a self-hosted inbox.
49
+ // Optional gates. Empty values are omitted, so a deploy of one inbox is unchanged.
53
50
  // WAKE_TARGET_POLICY=public-https refuses a wake that is not public https. The socket opens to an address from that check, and TLS names the hostname (see wake-target.ts).
54
51
  // APPROVAL_OIDC_* turn on OpenID Connect approval when issuer, client id, secret and allowlist are all set.
55
52
  TENANCY: v("TENANCY"),
@@ -78,8 +75,8 @@ if (hosted) {
78
75
  envBindings.TENANT_DO = bindings.durableObject({ worker: name, exportName: "TenantStore" });
79
76
  envBindings.TENANT_DIRECTORY = bindings.kv();
80
77
  }
81
- // Optional service binding to the control Worker. Unset: OpenID Connect keeps using fetch, which is
82
- // what a self-host deploy needs for an external issuer. Set A2A_CONTROL_SERVICE to that Worker's name
78
+ // Optional service binding for OpenID Connect discovery, token, and JWKS when the issuer shares
79
+ // the tenant zone. Unset: those calls use fetch. Set A2A_CONTROL_SERVICE to the issuer Worker's name
83
80
  // so a tenant on the same zone does not fetch the issuer through the public route.
84
81
  const controlService = v("A2A_CONTROL_SERVICE");
85
82
  if (controlService) envBindings.CONTROL_SERVICE = bindings.worker({ worker: controlService });
@@ -32,26 +32,10 @@ WAKE_PRESET=generic
32
32
  # UPSTREAM_ACCESS_CLIENT_ID / UPSTREAM_ACCESS_CLIENT_SECRET) go in a separate chmod-600
33
33
  # file passed with `cf deploy --secrets-file <file>`; never commit them.
34
34
  #
35
- # Optional multi-tenant gates. Leave them unset for a self-hosted inbox (`npx -y a2a-exposed@latest init`
36
- # never sets them). Unset means today's single-tenant Worker on D1, and the build does not export a Durable Object.
37
- # TENANCY=host switches the entry to the multi-tenant router and adds the TENANT_DO and TENANT_DIRECTORY bindings.
38
- # QUOTAS, USAGE_SINK, SIGNUP_URL and BRANDING are reserved (parsed, not enforced yet).
39
- # WAKE_TARGET_POLICY=public-https is enforced: a wake must be https to a public address. The Worker
35
+ # WAKE_TARGET_POLICY=public-https requires a wake to be https to a public address. The Worker
40
36
  # connects to an address from that check. A redirect is followed only on the same origin. Leave it
41
37
  # unset for a self-hosted inbox.
42
- # Do not set TENANCY for a self-hosted inbox.
43
- # TENANCY=
44
- # TENANT_DOMAIN=example.com
45
- # DATA_REGION=eu
46
- # Written onto a directory entry the first time that entry has no region. After that the stored region places the tenant.
47
- # QUOTAS=on
48
- # USAGE_SINK=d1
49
38
  # WAKE_TARGET_POLICY=public-https
50
- # SIGNUP_URL=https://example.com
51
- # BRANDING=hosted
52
- # https origin, no path. Only a hosted tenant (TENANCY=host) posts feedback there.
53
- # A self-hosted inbox ignores it and the CLI prints a GitHub issue link instead.
54
- # FEEDBACK_URL=https://control.example.com
55
39
  # OpenID Connect approval (optional). All four of issuer, client id, allowlist and the client secret
56
40
  # must be set or the pages stay password-only. APPROVAL_METHODS=oidc hides the password once that
57
41
  # config is complete; APPROVAL_METHODS=password keeps the password even if an issuer is set.
@@ -59,11 +43,7 @@ WAKE_PRESET=generic
59
43
  # APPROVAL_OIDC_CLIENT_ID=
60
44
  # APPROVAL_OIDC_ALLOWED_SUBJECTS=
61
45
  # APPROVAL_METHODS=password
62
- # Secrets (same secrets file, never here): APPROVAL_OIDC_CLIENT_SECRET, and for a hosted deploy TENANT_SECRETS_KEY.
63
- # Hosted approval uses the tenant's pushed approval object, not these Worker settings.
64
- # When that issuer is on the same zone as the tenant host, set this to the control Worker's name.
65
- # Discovery, token and JWKS then use the CONTROL_SERVICE binding. Unset: those calls use fetch.
66
- # A2A_CONTROL_SERVICE=a2a-exposed-control
46
+ # Secrets (same secrets file, never here): APPROVAL_OIDC_CLIENT_SECRET.
67
47
  # Daily SQL snapshots to R2. Unset: no BACKUP_BUCKET binding and no extra cron. The minute wake cron stays A2A_ENABLE_CRON.
68
48
  # A2A_BACKUP_BUCKET=1
69
49
  # A2A_BACKUP_BUCKET_NAME=a2a-exposed-backups
@@ -174,9 +174,9 @@ async function takeBody(res: Response, signal: AbortSignal, max: number): Promis
174
174
 
175
175
  /**
176
176
  * Discovery, token and JWKS for an issuer on this tenant's zone go through CONTROL_SERVICE.
177
- * A public fetch of that zone can miss the control Worker (wildcard route, 522, or the site Worker).
177
+ * A public fetch of that zone can miss the issuer (wildcard route, 522, or another Worker on the zone).
178
178
  * Any other issuer, and a deploy with the binding unset, uses fetch.
179
- * The binding request sets Host to the issuer host. Without that header the control Worker on the
179
+ * The binding request sets Host to the issuer host. Without that header an issuer on the
180
180
  * zone apex sees no Host, and the subrequest does not finish. Every call, binding or fetch, stops
181
181
  * after FETCH_MS and the page says the provider could not be reached.
182
182
  */
@@ -209,7 +209,7 @@ function publicFetch(url: string, init: RequestInit, signal: AbortSignal): Promi
209
209
  async function bindingFetch(binding: NonNullable<ReturnType<TenantContext["controlService"]>>, url: string, init: RequestInit, signal: AbortSignal): Promise<Response> {
210
210
  const headers = new Headers(init.headers);
211
211
  // `new Request(url)` does not keep Host across a service binding. The apex issuer is then
212
- // a request with no Host, which does not match the control Worker's route or ISSUER, and the
212
+ // a request with no Host, which does not match the issuer route or ISSUER, and the
213
213
  // subrequest never completes. Set Host to the URL host. `redirect` and `signal` stay off the
214
214
  // Request (they do not cross the binding). `signal` is passed to fetch so the runtime cancels it.
215
215
  headers.set("host", new URL(url).host);
@@ -1,4 +1,4 @@
1
- // Feedback from an owner-only MCP tool. A hosted tenant posts to the control plane.
1
+ // Feedback from an owner-only MCP tool. When a feedback URL is set, the owner posts there.
2
2
  // Any other deploy returns a prefilled GitHub issue URL and does not call out.
3
3
  import { reachesControl } from "./approval-oidc.ts";
4
4
  import { scrubSecretText } from "./export.ts";
@@ -247,7 +247,7 @@ async function postFeedback(ctx: TenantContext, endpoint: string, body: string,
247
247
  }
248
248
 
249
249
  /**
250
- * File the note when this tenant is hosted and the control plane accepts it.
250
+ * File the note when this tenant is hosted and the feedback URL accepts it.
251
251
  * No endpoint, or a 404, returns the prefilled issue URL and does not throw.
252
252
  * Anything else throws a short error with none of the message.
253
253
  */
@@ -1,17 +1,16 @@
1
- // Hosted entrypoint. Used only when the deploy sets TENANCY=host (see cloudflare.config.ts).
1
+ // Optional entrypoint. Used only when the deploy sets TENANCY=host (see cloudflare.config.ts).
2
2
  // With that set and a TENANT_DO binding, `<name>.<TENANT_DOMAIN>` is looked up in the KV directory and forwarded
3
- // to that tenant's SQLite Durable Object. The object runs the same inbox as self-host (`dispatch` in index.ts)
4
- // against its own database. Anything else — TENANCY unset, or no TENANT_DO binding — calls the self-host fetch
3
+ // to that name's SQLite Durable Object. The object runs the same inbox as a single deploy (`dispatch` in index.ts)
4
+ // against its own database. Anything else — TENANCY unset, or no TENANT_DO binding — calls the single-inbox fetch
5
5
  // handler on env.DB.
6
6
  //
7
- // Tenant config is a versioned row pushed into the object (`pushConfig`). It does not travel on the request.
7
+ // Config is a versioned row pushed into the object (`pushConfig`). It does not travel on the request.
8
8
  // An object with no row answers 404 and does not create application tables.
9
9
  //
10
- // Hosted wake debounce and the cron's housekeeping run from this object's alarm. The Worker minute cron stays a no-op
11
- // while TENANCY=host, so there is no shared database to flush. Self-host still uses its minute cron.
10
+ // Wake debounce and housekeeping run from this object's alarm. The Worker minute cron stays a no-op
11
+ // while TENANCY=host, so there is no shared database to flush. A single inbox still uses its minute cron.
12
12
  // The daily backup cron runs only when BACKUP_BUCKET is bound, and it returns before any flush.
13
- // Usage push is not in this change. Approval OIDC, when configured (self-host settings, or the pushed tenant
14
- // `approval` object), uses the same pages as self-host.
13
+ // OpenID Connect approval, when the issuer, client id, client secret, and allowlist are set, uses the same pages.
15
14
  import { DurableObject } from "cloudflare:workers";
16
15
  import worker, { cronFlush, dispatch, nextCronAt, onWakeWrite, runOwnerRpc } from "./index.ts";
17
16
  import { accountIdOf, type OwnerRpcRequest, type OwnerRpcResult } from "./owner-rpc.ts";
@@ -34,7 +33,7 @@ const gone = () => Response.json({ error: "this agent is gone" }, { status: 410,
34
33
 
35
34
  const TENANT_ID = /^[A-Za-z0-9._:-]{1,128}$/;
36
35
 
37
- /** Versioned config pushed by the control plane. `secretsEnc` is decrypted into the wake credentials when the context is built. */
36
+ /** Versioned config pushed into the object. `secretsEnc` is decrypted into the wake credentials when the context is built. */
38
37
  export interface TenantConfigPush {
39
38
  version: number;
40
39
  tenantId: string;
@@ -108,7 +108,7 @@ async function wakeBudgetOk(ctx: TenantContext): Promise<boolean> {
108
108
 
109
109
  const wakeWriteHooks = new WeakMap<object, () => Promise<void>>();
110
110
 
111
- /** A hosted tenant registers this so a wake row updates that tenant's alarm in the same turn as the write. */
111
+ /** A caller registers this so a wake row updates the alarm in the same turn as the write. */
112
112
  export function onWakeWrite(ectx: object, fn: () => Promise<void>): void {
113
113
  wakeWriteHooks.set(ectx, fn);
114
114
  }
@@ -204,7 +204,7 @@ async function wake(ctx: TenantContext, ectx: ExecutionContext, contextId: strin
204
204
  await noteWakeWrite(ectx);
205
205
  const delay = Math.max(0, (cur?.last_sent_ms || now) + deb - now) + 100;
206
206
  // waitUntil can outlive the response by ~30s. Longer windows are flushed by a later request, the optional
207
- // self-host cron, or the hosted tenant's alarm (set above).
207
+ // single-inbox cron, or the registered alarm (set above).
208
208
  if (delay <= 25000) ectx.waitUntil(new Promise((res) => setTimeout(res, delay)).then(() => flushWake(ctx, ectx, contextId)));
209
209
  }
210
210
 
@@ -255,7 +255,7 @@ async function minMs(ctx: TenantContext, sql: string, shift: (t: number) => numb
255
255
 
256
256
  /**
257
257
  * When the next flush or housekeeping delete becomes due, or null when nothing is waiting.
258
- * A hosted tenant sets its alarm to this time. Self-host keeps the minute cron and does not call this.
258
+ * Per-inbox storage sets its alarm to this time. A single inbox keeps the minute cron and does not call this.
259
259
  */
260
260
  export async function nextCronAt(ctx: TenantContext, now = Date.now()): Promise<number | null> {
261
261
  const times: number[] = [];
@@ -2180,7 +2180,7 @@ async function mcpGrantOf(ctx: TenantContext, header: string | null): Promise<Js
2180
2180
  }
2181
2181
 
2182
2182
  /**
2183
- * Hosted owner RPC. The control plane has already checked that `accountId` owns this tenant.
2183
+ * Hosted owner RPC. The caller has already checked that `accountId` owns this tenant.
2184
2184
  * Only an allowlisted path runs, and the response is withheld when it contains a peer token or a secret.
2185
2185
  * Self-host (`mode` single) is refused here; its owner API stays the bearer check in `handleOwner`.
2186
2186
  */
@@ -1,12 +1,12 @@
1
- // Hosted owner RPC allowlist. The control plane calls TenantStore.owner() after it has checked that
2
- // the account owns the tenant. This list is the only paths that call may run, and it is stricter than
1
+ // Hosted owner RPC allowlist. The caller has checked that the account owns the tenant.
2
+ // This list is the only paths that call may run, and it is stricter than
3
3
  // the owner HTTP API: routes that mint a peer token, return a setup secret, or accept a plaintext peer
4
4
  // token are not here. GET /owner/export is here only because runOwnerRpc redacts it first. Self-host never calls this.
5
5
 
6
6
  import { ID_RE } from "./a2a.ts";
7
7
  import { credentialIsMasked, previewHeaderPlain } from "./wake.ts";
8
8
 
9
- /** Control-plane account id. Slashes and dots are refused so the value cannot be a path. */
9
+ /** Account id. Slashes and dots are refused so the value cannot be a path. */
10
10
  export const ACCOUNT_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
11
11
 
12
12
  const LABEL = /^[A-Za-z0-9_-]{1,32}$/;
@@ -42,7 +42,7 @@ export interface OwnerRpcRequest {
42
42
  /** Refused. The owner HTTP API is where a body is accepted. */
43
43
  body?: unknown;
44
44
  /**
45
- * Dashboard approve or deny after the control plane has checked a recent sign-in.
45
+ * Approve or deny after a recent sign-in.
46
46
  * Any other value is refused. Absent keeps the owner HTTP API's actor (`agent` or `owner`).
47
47
  */
48
48
  actor?: "human";
@@ -1,6 +1,6 @@
1
1
  // D1-shaped database over a Durable Object's SQLite (`ctx.storage.sql`).
2
2
  // Handlers keep calling prepare().bind().first()/all()/run() and batch(). Self-host keeps the real D1 binding;
3
- // a hosted tenant uses this adapter on its own object. `migrateDo` matches `cf d1 migrations apply`: the same
3
+ // a per-inbox Durable Object uses this adapter on its own object. `migrateDo` matches `cf d1 migrations apply`: the same
4
4
  // `d1_migrations` table, unrecorded files in numeric order, one transaction per file.
5
5
 
6
6
  /** The slice of the Workers `SqlStorage` API this adapter uses. Tests can implement it over node:sqlite. */
@@ -1,15 +1,14 @@
1
- // Tenant context for the data-plane Worker.
1
+ // Context for one inbox.
2
2
  //
3
- // Self-host (TENANCY unset, the default): one implicit tenant, config and OWNER_TOKEN taken from the Worker env.
3
+ // Default (TENANCY unset): one inbox. Config and OWNER_TOKEN come from the Worker env.
4
4
  // Handlers receive this object instead of reading env.* themselves. With every gate unset, owner checks and the
5
5
  // outbound peer-token key are the same operations as before (existing ciphertext still decrypts).
6
6
  //
7
- // Hosted (a TenantContext built with a pushed tenant overlay): owner auth is a constant-time compare against
8
- // owner_token_hash, and the peer-token key is derived from TENANT_SECRETS_KEY and the tenant id. The Worker
9
- // fetch path does not build that overlay; the hosted router does, and only when TENANCY=host and TENANT_DO are
10
- // both set. Settings below are parsed either way. Nothing here turns on quotas, metering, wake policy, branding
11
- // those stay reserved until a later change reads ctx.gates. Approval OIDC is enforced by the approval pages when
12
- // the issuer, client id, client secret and an allowlist are all set (approval-oidc.ts).
7
+ // When a pushed config is set, and only when TENANCY is "host" and TENANT_DO is bound, owner auth compares a
8
+ // token hash and the peer-token key is derived from TENANT_SECRETS_KEY and the inbox id. The fetch path does not
9
+ // build that overlay. Settings below are parsed either way. Some of them stay unused until a caller reads
10
+ // ctx.gates. OpenID Connect approval is enforced by the approval pages when the issuer, client id, client secret,
11
+ // and an allowlist are all set (approval-oidc.ts).
13
12
 
14
13
  import * as A from "./a2a.ts";
15
14
 
@@ -46,9 +45,9 @@ export type ConfigKey = (typeof CONFIG_KEYS)[number];
46
45
  export type ConfigFields = { [K in ConfigKey]?: string };
47
46
 
48
47
  /**
49
- * Non-secret platform defaults a hosted tenant inherits when its pushed config omits them.
50
- * Wake secrets, upstream secrets and OWNER_TOKEN are never inherited. In hosted mode the secret wake
51
- * fields come from decrypting `secrets_enc` (see the tenant object). A plaintext copy in the pushed
48
+ * Non-secret defaults used when a pushed config omits them.
49
+ * Wake secrets, upstream secrets, and OWNER_TOKEN are never copied. Secret wake
50
+ * fields come from decrypting `secrets_enc`. A plaintext copy in the pushed
52
51
  * config is used only when that ciphertext is absent.
53
52
  */
54
53
  const HOSTED_PLATFORM_DEFAULTS: readonly ConfigKey[] = ["MAX_BODY", "RATE_PER_MIN", "PBKDF2_ITERATIONS", "MCP", "PAIRING_APPROVAL"];
@@ -86,17 +85,17 @@ export interface WorkerBindings extends ConfigFields {
86
85
  TENANT_DO?: DoNamespace;
87
86
  /** unset | eu | fedramp. Applied as a DO jurisdiction when the hosted router addresses a tenant. */
88
87
  DATA_REGION?: string;
89
- /** "on" reserves quota enforcement (not applied yet). */
88
+ /** Parsed and unused by this Worker. */
90
89
  QUOTAS?: string;
91
- /** "d1" reserves usage metering (not applied yet). */
90
+ /** Parsed and unused by this Worker. */
92
91
  USAGE_SINK?: string;
93
92
  /** "public-https" refuses non-https, private, and metadata wake targets. Anything else is unset. */
94
93
  WAKE_TARGET_POLICY?: string;
95
- /** https URL shown later on the landing page and over-quota errors. Parsed, not rendered yet. */
94
+ /** https URL. Parsed and unused by this Worker. */
96
95
  SIGNUP_URL?: string;
97
- /** https origin, no path. A hosted tenant posts feedback there. Ignored on a self-hosted inbox. */
96
+ /** https origin, no path. Posts go there when this deploy sets the URL. */
98
97
  FEEDBACK_URL?: string;
99
- /** "hosted" reserves footer / plan badge. Parsed, not rendered yet. */
98
+ /** Parsed string. Not rendered by this Worker. */
100
99
  BRANDING?: string;
101
100
  APPROVAL_OIDC_ISSUER?: string;
102
101
  APPROVAL_OIDC_CLIENT_ID?: string;
@@ -106,8 +105,8 @@ export interface WorkerBindings extends ConfigFields {
106
105
  /** Comma-separated: password, oidc. Unset keeps the password and also offers OIDC once it is fully configured. */
107
106
  APPROVAL_METHODS?: string;
108
107
  /**
109
- * Service binding to the control Worker. Used for OpenID Connect discovery, token and JWKS
110
- * when the issuer is on this tenant's zone. Unset: those calls use `fetch` (self-host, and any other issuer).
108
+ * Service binding for OpenID Connect discovery, token, and JWKS when the issuer is on this
109
+ * tenant's zone. Unset: those calls use `fetch`.
111
110
  */
112
111
  CONTROL_SERVICE?: ControlFetcher;
113
112
  }
@@ -145,15 +144,15 @@ export interface ApprovalOidcGate {
145
144
  methods: string[];
146
145
  /** True only when the setting listed at least one of password or oidc. Unset is false. */
147
146
  methodsSpecified: boolean;
148
- /** Short button label from hosted approval config, or "". */
147
+ /** Short button label from the pushed approval config, or "". */
149
148
  label: string;
150
149
  }
151
150
 
152
151
  export interface FeatureGates {
153
152
  tenancy: TenancyMode;
154
153
  /**
155
- * True only when TENANCY is exactly "host" and TENANT_DO is bound. That is the switch for per-tenant storage.
156
- * Any other combination, including TENANCY=host alone, keeps the self-host path on env.DB.
154
+ * True only when TENANCY is exactly "host" and TENANT_DO is bound. That switches storage off env.DB.
155
+ * Any other combination, including TENANCY=host alone, keeps the single-inbox path on env.DB.
157
156
  */
158
157
  hostedStorage: boolean;
159
158
  quotas: boolean;
@@ -161,20 +160,20 @@ export interface FeatureGates {
161
160
  wakeTargetPolicy: "" | "public-https";
162
161
  signupUrl: string;
163
162
  branding: "" | "hosted";
164
- /** https origin of a control plane that accepts POST /api/feedback, or "". Ignored unless mode is host. */
163
+ /** https origin that accepts POST /api/feedback, or "". Ignored unless mode is host. */
165
164
  feedbackUrl: string;
166
165
  dataRegion: DataRegion;
167
166
  tenantDomain: string;
168
167
  approvalOidc: ApprovalOidcGate;
169
168
  }
170
169
 
171
- /** Pushed tenant row (control plane → DO). PR1 tests build a context from this; the DO stores it in PR2. */
170
+ /** Pushed tenant row. Tests build a context from this; the Durable Object stores it. */
172
171
  export interface HostedOverlay {
173
172
  id: string;
174
173
  db: SqlDb;
175
174
  publicUrl: string;
176
175
  ownerTokenHash?: string | null;
177
- /** Non-secret and, when the control plane includes them, secret config fields. Never OWNER_TOKEN. */
176
+ /** Non-secret and secret config fields. Never OWNER_TOKEN. */
178
177
  config?: Record<string, string | undefined>;
179
178
  limits?: Record<string, unknown>;
180
179
  approval?: Record<string, unknown>;
@@ -252,7 +251,7 @@ function subjectsOf(v: unknown): string[] {
252
251
  return list.map((s) => s.trim()).filter(Boolean).slice(0, 100);
253
252
  }
254
253
 
255
- /** Approval gate from a hosted tenant's pushed `approval` object (camelCase). The client secret is returned beside the gate. */
254
+ /** Approval gate from a pushed `approval` object (camelCase). The client secret is returned beside the gate. */
256
255
  export function approvalFromRecord(raw: Record<string, unknown> | undefined): { gate: ApprovalOidcGate; clientSecret: string; publicRecord: Record<string, unknown> } {
257
256
  const { methods, specified } = recognizedMethods(Array.isArray(raw?.methods) ? raw!.methods.map((s) => String(s)) : typeof raw?.methods === "string" ? csv(raw.methods) : []);
258
257
  const clientSecret = typeof raw?.clientSecret === "string" ? raw.clientSecret.trim() : "";
@@ -376,7 +375,7 @@ export class TenantContext implements ConfigFields {
376
375
  readonly #secretsKey?: string;
377
376
  /** OpenID Connect client secret. Never logged and never copied onto `gates`. */
378
377
  readonly #approvalClientSecret?: string;
379
- /** Control Worker binding. Not a secret. Absent on a self-host deploy. */
378
+ /** Service binding for the issuer. Not a secret. Absent on a self-host deploy. */
380
379
  readonly #controlService?: ControlFetcher;
381
380
  #peerKey: Promise<CryptoKey | null> | null = null;
382
381
 
@@ -407,7 +406,7 @@ export class TenantContext implements ConfigFields {
407
406
  return this.#approvalClientSecret || "";
408
407
  }
409
408
 
410
- /** Control Worker binding, or undefined when this deploy has none. */
409
+ /** Issuer service binding, or undefined when this deploy has none. */
411
410
  controlService(): ControlFetcher | undefined {
412
411
  return this.#controlService;
413
412
  }
@@ -1,2 +1,2 @@
1
1
  /** Inbox build. Keep this equal to `version` in cli/package.json. */
2
- export const VERSION = "0.7.34";
2
+ export const VERSION = "0.8.0";
@@ -1,5 +1,5 @@
1
- // Wake secrets sealed for one hosted tenant. The control plane writes `secrets_enc`; the tenant
2
- // object decrypts it when it builds the tenant context. Self-host does not use this.
1
+ // Wake secrets sealed for one inbox record. The writer stores `secrets_enc`; the inbox
2
+ // object decrypts it when it builds the context. A single-inbox deploy does not use this.
3
3
  // Keep control/src/tenants/wake-secrets.ts in step with this file.
4
4
 
5
5
  const INFO = "tenant-wake-secrets v1";
@@ -271,7 +271,7 @@ test("hosted approval reaches a same-zone issuer through CONTROL_SERVICE", async
271
271
  return new Response("missing", { status: 404 });
272
272
  },
273
273
  },
274
- // A public fetch of another host on this zone does not reach the control Worker. If it is attempted, answer 522.
274
+ // A public fetch of another host on this zone does not reach the issuer. If it is attempted, answer 522.
275
275
  outboundService: async (request: Request) => {
276
276
  publicHosts.push(new URL(request.url).hostname);
277
277
  return new Response("origin is unreachable", { status: 522 });
@@ -367,7 +367,7 @@ test("hosted approval reaches a same-zone issuer through CONTROL_SERVICE", async
367
367
  assert.match(await missedPost.text(), /The identity provider could not be reached/);
368
368
  });
369
369
 
370
- test("an apex issuer is fetched with that Host, and a control worker that does not answer still returns", async (t) => {
370
+ test("an apex issuer is fetched with that Host, and a binding that does not answer still returns", async (t) => {
371
371
  const issuer = "https://example.com";
372
372
  const tenant = "https://northwind.example.com";
373
373
  const control = `