diff --git a/services/api/.dev.vars.example b/services/api/.dev.vars.example index f9018eb6..773afa2f 100644 --- a/services/api/.dev.vars.example +++ b/services/api/.dev.vars.example @@ -3,4 +3,5 @@ GITHUB_APP_INSTALLATION_ID="your-installation-id" GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" ASSERTION_SIGNING_SECRET="local-shared-secret" # Must match `FOSSBilling/extensions` EXTENSIONS_REVALIDATE_SECRET="local-revalidate-secret" -EXTENSIONS_V2_MXROUTE_PASSWORD="local-mailbox-password" +EXTENSIONS_EMAIL_PROVIDER="disabled" +EXTENSIONS_EMAIL_PASSWORD="local-email-password" diff --git a/services/api/README.md b/services/api/README.md index a7f13fe6..07128441 100644 --- a/services/api/README.md +++ b/services/api/README.md @@ -86,7 +86,7 @@ We use [Cloudflare D1](https://developers.cloudflare.com/d1/) and [KV](https://d ### Environment Variables -Production B2 connection details and MXroute server/username are managed in the +Production B2 connection details and email server/username are managed in the Worker dashboard under **Settings > Variables and Secrets** as runtime variables. Credentials are stored as secrets. Build environment variables do not configure the running Worker. `keep_vars: true` preserves dashboard-managed variables on @@ -103,12 +103,11 @@ Package storage configuration is listed in [PACKAGING.md](src/services/extension - `ASSERTION_SIGNING_SECRET`: Shared HMAC secret used to verify the short-lived bearer assertions minted by the Extensions site. Configure the same value in both Workers; it is never sent to clients. - `ASSERTION_SIGNING_SECRET_PREVIOUS`: Optional previous HMAC secret accepted during a signing-key rotation. - `EXTENSIONS_REVALIDATE_SECRET`: Bearer token the API sends to the Extensions site's `POST /api/revalidate` (over the `EXTENSIONS_FRONTEND` service binding) to purge the site's CDN-cached extension pages after content mutations. Configure the same value in both Workers; it is never sent to clients. -- `EXTENSIONS_V2_EMAIL_PROVIDER`: `mxroute` or `resend` to send moderator notification emails, or unset/`disabled` to skip sending. -- `EXTENSIONS_V2_EMAIL_FROM`: Sender address for notification emails (default `noreply@fossbilling.org`). -- `EXTENSIONS_V2_EMAIL_REPLY_TO`: Optional Reply-To address for a monitored inbox. Omit when unset (no Reply-To header is sent) — notification emails do not invite replies. -- `EXTENSIONS_V2_MXROUTE_SERVER`, `EXTENSIONS_V2_MXROUTE_USERNAME`: Dashboard-managed runtime variables identifying the mailbox for the MXroute SMTP API (`https://smtpapi.mxroute.com/`). Required when `EXTENSIONS_V2_EMAIL_PROVIDER=mxroute`. -- `EXTENSIONS_V2_MXROUTE_PASSWORD`: Mailbox password, stored as a secret. Required when `EXTENSIONS_V2_EMAIL_PROVIDER=mxroute`. -- `EXTENSIONS_V2_RESEND_API_KEY`: API key for Resend. Required when `EXTENSIONS_V2_EMAIL_PROVIDER=resend`. +- `EXTENSIONS_EMAIL_PROVIDER`: Production secret managed in the Worker dashboard. Local development sets `disabled` in `.dev.vars`; Previews set `disabled` in `wrangler.jsonc`. Use `mxroute` or `resend` to send moderator notification emails, or unset/`disabled` to skip sending. +- `EXTENSIONS_EMAIL_FROM`: Sender address for notification emails (default `noreply@fossbilling.org`). +- `EXTENSIONS_EMAIL_REPLY_TO`: Optional Reply-To address for a monitored inbox. Omit when unset (no Reply-To header is sent) — notification emails do not invite replies. +- `EXTENSIONS_EMAIL_SERVER`, `EXTENSIONS_EMAIL_USERNAME`: Mail server and login used by the MXroute adapter. Required for `mxroute`; unused by `resend`. +- `EXTENSIONS_EMAIL_PASSWORD`: Secret containing the mailbox password for `mxroute` or API key for `resend`. - `EXTENSIONS_RETENTION_MODE`: `dry-run` (report only) or `compact` (prune old revision bodies). See [extensions v2 README](src/services/extensions/v2/README.md#revisions-and-retention). - `M2M_ISSUER`, `M2M_JWKS_URL`, `M2M_AUDIENCE`: Issuer, JWKS endpoint, and expected audience for machine-to-machine access tokens minted by the central auth service (`FOSSBilling/auth`). Defaults live in `wrangler.jsonc`; preview environments need them pointed at the issuing preview. See [extensions v2 README](src/services/extensions/v2/README.md#machine-publishing). @@ -181,22 +180,40 @@ Validation and artifact builds run on branch pushes and PRs. A push can deploy o after workspace validation, the build and D1 migrations succeed. `main` targets production; other branches target Previews once `API_PREVIEW_RESOURCES` is configured. Until then, branch builds report that Preview migrations and deployment are skipped; -validation and artifact checks still run. PR runs, including forks, never receive -deployment credentials or apply remote migrations. +validation and artifact checks still run. The configured PR jobs, including forks, +run validation and build checks without forwarding deployment credentials or +applying remote migrations. The artifact contains the compiled Worker, `wrangler.json`, and SQL migrations. -Deployment does not rebuild the Worker. Production migrations use that artifact's -production bindings. Preview migrations use its generated +Deployment uses the same Node version as the build and does not rebuild the Worker. +Production migrations use its production bindings. Preview migrations use its generated `wrangler.preview-migrations.json`, derived from the same bindings used by the Preview. Migration jobs serialize access to the shared staging databases. -#### Setup before switching deployment +#### Deployment setup + +1. Set `CLOUDFLARE_API_TOKEN` as a repository secret and `CLOUDFLARE_ACCOUNT_ID` + as a repository variable. Configure GitHub `production` and `preview` + environments, and restrict `production` to `main`. + + Like `fossbilling.org`, this setup trusts repository maintainers with branch + push access to use the deployment credential. Repository secrets are available + to workflows those maintainers can modify; environment restrictions and the + workflow's `main` condition select deployment targets but do not protect a + repository secret from branch workflows. Fork PR runs do not receive the + deployment token. + + Scope the token to the API Worker and its zone. It needs + `Individual Workers Editor` for that Worker, `Workers Routes Write` for route changes, `D1 Read` + and `D1 Write` for migrations, and `Queues Read` and `Queues Write` to configure + the existing consumer. + The Worker permission covers both production and Previews. Separate staging data + and runtime secrets isolate test state, not the deployment credential's authority. + + If branch writers must not have production deployment authority, use protected + environment secrets with trusted deployment approval instead. That is a + different access policy, not a requirement of this trusted-maintainer setup. -1. Configure GitHub `production` and `preview` environments, each with its own - `CLOUDFLARE_API_TOKEN` secret. Set `CLOUDFLARE_ACCOUNT_ID` as a repository variable - shared by both deployment targets. Restrict the production environment to `main`. - Deployment tokens need Workers deployment and - D1 migration permissions; Preview credentials and resources must be non-production. 2. Provision staging D1 databases, KV namespaces, a download bucket and a separately deployed staging Extensions frontend. Set the repository variable `API_PREVIEW_RESOURCES` to their identifiers, using the following JSON shape: @@ -219,19 +236,22 @@ Migration jobs serialize access to the shared staging databases. staging resources; they do not automatically receive separate D1/KV/R2 data. 3. Configure Preview runtime secrets in the API Worker's **Previews Base** settings. - Use a separate assertion-signing secret, revalidation secret and GitHub App - preview key, alongside `GITHUB_APP_CLIENT_ID` and `GITHUB_APP_INSTALLATION_ID` - (see [GitHub App authentication](#github-app-authentication)). Match the - revalidation secret with the staging frontend. Set M2M issuer, + Use separate assertion-signing and revalidation secrets, matching both with + the staging frontend. Set all three `GITHUB_APP_*` values, selecting the preview + private key (see [GitHub App authentication](#github-app-authentication)). Set M2M issuer, JWKS and audience variables there when using a separate test issuer. Operational production variables and secrets remain dashboard-managed with `keep_vars: true`. -4. Configure required checks for the new validation/build job names, then disable the +4. Configure required checks for the validation/build jobs, then disable the API's Cloudflare Workers Builds Git integration to avoid duplicate deployments. The workflow compiles the branch's Preview settings into the artifact. Each Preview receives isolated Durable Object state; the data bindings above are shared staging resources. A frontend service binding calls that Worker's normal deployment, so use a -staging Worker name rather than the production Extensions worker. +staging Worker name rather than the production Extensions worker. Configure that +frontend with HTTP API transport and a Preview API URL, without production routes +or API bindings. A long-lived Preview such as `staging` can back this frontend; +refresh it from the prepared artifact with `npm run cf-preview --workspace @fossbilling/api -- --name staging`. +Register its OAuth callback with a dedicated test client before testing sign-in. Cloudflare Previews cannot consume Queues or receive Cron Triggers. Their configuration therefore disables packaging and notification email, and omits Queue bindings. Full @@ -253,3 +273,19 @@ before running `npm run cf-preview`; CI performs those steps automatically. `npm run cf-deploy` remains a deliberate manual production command. It applies remote migrations and deploys through Wrangler. Neither command deploys the separate [extension packager](../extension-packager/README.md). Production packaging remains disabled. + +A dashboard change can trigger Wrangler's remote configuration comparison. If its +custom-domain metadata lookup returns 403 with the Worker-scoped CI token, use a +one-off local-auth deployment rather than broadening the token. Build artifacts +expire after one day; after expiry, rerun the full CI workflow to rebuild them. +After validation, build and migrations pass, unset `CLOUDFLARE_API_TOKEN` locally. +Use an existing Wrangler OAuth login to deploy that `main` run's artifact (replace `RUN_ID`): + +```sh +gh run download RUN_ID --name api-worker-build --dir .wrangler/deployment +npx wrangler deploy --config .wrangler/deployment/wrangler.json --keep-vars +``` + +Download into an empty directory, keep `EXTENSION_PACKAGES_MODE=disabled`, then +rerun the failed CI deployment job. Further dashboard changes can trigger the same +comparison again. diff --git a/services/api/src/services/extensions/v2/README.md b/services/api/src/services/extensions/v2/README.md index 30162df1..19fc1388 100644 --- a/services/api/src/services/extensions/v2/README.md +++ b/services/api/src/services/extensions/v2/README.md @@ -160,7 +160,7 @@ For organization developer IDs, GitHub membership auto-verifies only against a v Revision approve/reject, delist/relist, developer approve, and claim approve/reject email the affected author unless the moderator passes `?notify=false`. Bot decisions use the same path, marked with an `[auto policy=…]` `review_note` prefix. -Recipients: developer `contact_email`, falling back to the owning account's email; claim decisions go to the claimant's account email. Sending is best-effort via `waitUntil` and never fails the write; the result carries `notified: boolean`. See `email/` for the provider abstraction (`mxroute`, `resend`, `disabled`) and the root README for `EXTENSIONS_V2_EMAIL_*`. +Recipients: developer `contact_email`, falling back to the owning account's email; claim decisions go to the claimant's account email. Sending is best-effort via `waitUntil` and never fails the write; the result carries `notified: boolean`. See `email/` for the provider abstraction (`mxroute`, `resend`, `disabled`) and the root README for `EXTENSIONS_EMAIL_*`. ## Limits and budgets diff --git a/services/api/src/services/extensions/v2/email/config.ts b/services/api/src/services/extensions/v2/email/config.ts index 818285d8..0012b5a6 100644 --- a/services/api/src/services/extensions/v2/email/config.ts +++ b/services/api/src/services/extensions/v2/email/config.ts @@ -9,7 +9,7 @@ export type EmailProviderName = "mxroute" | "resend"; // namespace (see DB_EXTENSIONS), so an unscoped EMAIL_FROM would collide // with the next service that sends mail. export function resolveEmailProvider(env: EnvReader): EmailProviderName | null { - switch (env.getEnv("EXTENSIONS_V2_EMAIL_PROVIDER")?.toLowerCase()) { + switch (env.getEnv("EXTENSIONS_EMAIL_PROVIDER")?.toLowerCase()) { case "mxroute": return "mxroute"; case "resend": @@ -20,10 +20,10 @@ export function resolveEmailProvider(env: EnvReader): EmailProviderName | null { } export function loadEmailIdentity(env: EnvReader): EmailIdentity { - // Blank counts as unset: an empty EXTENSIONS_V2_EMAIL_FROM would otherwise + // Blank counts as unset: an empty EXTENSIONS_EMAIL_FROM would otherwise // become an invalid sender address instead of falling back to the default. - const from = env.getEnv("EXTENSIONS_V2_EMAIL_FROM")?.trim(); - const replyTo = env.getEnv("EXTENSIONS_V2_EMAIL_REPLY_TO")?.trim(); + const from = env.getEnv("EXTENSIONS_EMAIL_FROM")?.trim(); + const replyTo = env.getEnv("EXTENSIONS_EMAIL_REPLY_TO")?.trim(); return { from: from ? from : "noreply@fossbilling.org", replyTo: replyTo ? replyTo : undefined diff --git a/services/api/src/services/extensions/v2/email/mxroute.ts b/services/api/src/services/extensions/v2/email/mxroute.ts index c212d7dc..41eacc29 100644 --- a/services/api/src/services/extensions/v2/email/mxroute.ts +++ b/services/api/src/services/extensions/v2/email/mxroute.ts @@ -22,9 +22,9 @@ export function loadMxrouteConfig( env: EnvReader, identity: EmailIdentity ): MxrouteConfig | null { - const server = env.getEnv("EXTENSIONS_V2_MXROUTE_SERVER"); - const username = env.getEnv("EXTENSIONS_V2_MXROUTE_USERNAME"); - const password = env.getEnv("EXTENSIONS_V2_MXROUTE_PASSWORD"); + const server = env.getEnv("EXTENSIONS_EMAIL_SERVER"); + const username = env.getEnv("EXTENSIONS_EMAIL_USERNAME"); + const password = env.getEnv("EXTENSIONS_EMAIL_PASSWORD"); if (!server || !username || !password) return null; return { ...identity, server, username, password }; } diff --git a/services/api/src/services/extensions/v2/email/resend.ts b/services/api/src/services/extensions/v2/email/resend.ts index e67a6d40..5260eca8 100644 --- a/services/api/src/services/extensions/v2/email/resend.ts +++ b/services/api/src/services/extensions/v2/email/resend.ts @@ -19,14 +19,14 @@ export function loadResendConfig( env: EnvReader, identity: EmailIdentity ): ResendConfig | null { - const apiKey = env.getEnv("EXTENSIONS_V2_RESEND_API_KEY"); + const apiKey = env.getEnv("EXTENSIONS_EMAIL_PASSWORD"); if (!apiKey) return null; return { ...identity, apiKey }; } -// Kept beside the MXroute sender so EXTENSIONS_V2_EMAIL_PROVIDER can switch -// without route changes. Activated by EXTENSIONS_V2_EMAIL_PROVIDER=resend -// with EXTENSIONS_V2_RESEND_API_KEY set. +// Kept beside the MXroute sender so EXTENSIONS_EMAIL_PROVIDER can switch +// without route changes. Activated by EXTENSIONS_EMAIL_PROVIDER=resend +// with EXTENSIONS_EMAIL_PASSWORD set. export class ResendSender implements EmailSender { constructor( private config: ResendConfig, diff --git a/services/api/test/services/extensions/v2/email.test.ts b/services/api/test/services/extensions/v2/email.test.ts index c7b45e01..597eb51a 100644 --- a/services/api/test/services/extensions/v2/email.test.ts +++ b/services/api/test/services/extensions/v2/email.test.ts @@ -26,10 +26,10 @@ function reader(vars: Record): EnvReader { const IDENTITY = { from: "extensions@fossbilling.org" }; const MXROUTE_VARS = { - EXTENSIONS_V2_EMAIL_PROVIDER: "mxroute", - EXTENSIONS_V2_MXROUTE_SERVER: "tuesday.mxrouting.net", - EXTENSIONS_V2_MXROUTE_USERNAME: "extensions@fossbilling.org", - EXTENSIONS_V2_MXROUTE_PASSWORD: "secret" + EXTENSIONS_EMAIL_PROVIDER: "mxroute", + EXTENSIONS_EMAIL_SERVER: "tuesday.mxrouting.net", + EXTENSIONS_EMAIL_USERNAME: "extensions@fossbilling.org", + EXTENSIONS_EMAIL_PASSWORD: "secret" }; function jsonResponse(payload: unknown, status = 200): Response { @@ -40,16 +40,16 @@ describe("email config", () => { it("resolves no provider by default", () => { expect(resolveEmailProvider(reader({}))).toBeNull(); expect( - resolveEmailProvider(reader({ EXTENSIONS_V2_EMAIL_PROVIDER: "pigeon" })) + resolveEmailProvider(reader({ EXTENSIONS_EMAIL_PROVIDER: "pigeon" })) ).toBeNull(); }); it("resolves the selected provider case-insensitively", () => { expect( - resolveEmailProvider(reader({ EXTENSIONS_V2_EMAIL_PROVIDER: "MXRoute" })) + resolveEmailProvider(reader({ EXTENSIONS_EMAIL_PROVIDER: "MXRoute" })) ).toBe("mxroute"); expect( - resolveEmailProvider(reader({ EXTENSIONS_V2_EMAIL_PROVIDER: "resend" })) + resolveEmailProvider(reader({ EXTENSIONS_EMAIL_PROVIDER: "resend" })) ).toBe("resend"); }); @@ -61,8 +61,8 @@ describe("email config", () => { expect( loadEmailIdentity( reader({ - EXTENSIONS_V2_EMAIL_FROM: "extensions@fossbilling.org", - EXTENSIONS_V2_EMAIL_REPLY_TO: "extensions@fossbilling.org" + EXTENSIONS_EMAIL_FROM: "extensions@fossbilling.org", + EXTENSIONS_EMAIL_REPLY_TO: "extensions@fossbilling.org" }) ) ).toEqual({ @@ -70,7 +70,7 @@ describe("email config", () => { replyTo: "extensions@fossbilling.org" }); expect( - loadEmailIdentity(reader({ EXTENSIONS_V2_EMAIL_FROM: " " })).from + loadEmailIdentity(reader({ EXTENSIONS_EMAIL_FROM: " " })).from ).toBe("noreply@fossbilling.org"); }); @@ -83,7 +83,7 @@ describe("email config", () => { }); expect( loadMxrouteConfig( - reader({ EXTENSIONS_V2_EMAIL_PROVIDER: "mxroute" }), + reader({ EXTENSIONS_EMAIL_PROVIDER: "mxroute" }), IDENTITY ) ).toBeNull(); @@ -92,7 +92,7 @@ describe("email config", () => { it("loads a complete resend config and rejects a missing key", () => { expect( loadResendConfig( - reader({ EXTENSIONS_V2_RESEND_API_KEY: "re_key" }), + reader({ EXTENSIONS_EMAIL_PASSWORD: "re_key" }), IDENTITY ) ).toEqual({ ...IDENTITY, apiKey: "re_key" }); @@ -190,7 +190,7 @@ describe("MxrouteSender", () => { describe("ResendSender", () => { const config = loadResendConfig( - reader({ EXTENSIONS_V2_RESEND_API_KEY: "re_key" }), + reader({ EXTENSIONS_EMAIL_PASSWORD: "re_key" }), IDENTITY )!; @@ -235,7 +235,7 @@ describe("factory", () => { ); const incompleteResend = createEmailSender( - reader({ EXTENSIONS_V2_EMAIL_PROVIDER: "resend" }) + reader({ EXTENSIONS_EMAIL_PROVIDER: "resend" }) ); expect(incompleteResend).toBeInstanceOf(DisabledSender); await expect( @@ -246,7 +246,7 @@ describe("factory", () => { }); const incomplete = createEmailSender( - reader({ EXTENSIONS_V2_EMAIL_PROVIDER: "mxroute" }) + reader({ EXTENSIONS_EMAIL_PROVIDER: "mxroute" }) ); expect(incomplete).toBeInstanceOf(DisabledSender); await expect( @@ -292,7 +292,7 @@ describe("default fetch binding", () => { ).resolves.toEqual({ ok: true }); const resendConfig = loadResendConfig( - reader({ EXTENSIONS_V2_RESEND_API_KEY: "re_key" }), + reader({ EXTENSIONS_EMAIL_PASSWORD: "re_key" }), IDENTITY )!; await expect( diff --git a/services/api/test/services/extensions/v2/moderation-notify.test.ts b/services/api/test/services/extensions/v2/moderation-notify.test.ts index 33022c8c..23ac5a53 100644 --- a/services/api/test/services/extensions/v2/moderation-notify.test.ts +++ b/services/api/test/services/extensions/v2/moderation-notify.test.ts @@ -30,11 +30,11 @@ vi.mock("@octokit/request", async () => setupExtensionsV2Tests(); const MXROUTE_ENV = { - EXTENSIONS_V2_EMAIL_PROVIDER: "mxroute", - EXTENSIONS_V2_EMAIL_FROM: "extensions@fossbilling.org", - EXTENSIONS_V2_MXROUTE_SERVER: "tuesday.mxrouting.net", - EXTENSIONS_V2_MXROUTE_USERNAME: "extensions@fossbilling.org", - EXTENSIONS_V2_MXROUTE_PASSWORD: "secret" + EXTENSIONS_EMAIL_PROVIDER: "mxroute", + EXTENSIONS_EMAIL_FROM: "extensions@fossbilling.org", + EXTENSIONS_EMAIL_SERVER: "tuesday.mxrouting.net", + EXTENSIONS_EMAIL_USERNAME: "extensions@fossbilling.org", + EXTENSIONS_EMAIL_PASSWORD: "secret" }; type EmailEnvKey = keyof typeof MXROUTE_ENV; diff --git a/services/api/worker-configuration.d.ts b/services/api/worker-configuration.d.ts index 6ecc9121..926e55d8 100644 --- a/services/api/worker-configuration.d.ts +++ b/services/api/worker-configuration.d.ts @@ -1,6 +1,6 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types --env-file=.dev.vars.example --env-interface=CloudflareBindings` (hash: 8625c7a110081a79725d5824556790df) -// Runtime types generated with workerd@1.20261104.1 2026-10-01 +// Generated by Wrangler by running `wrangler types --env-file=.dev.vars.example --env-interface=CloudflareBindings` (hash: 0aa56d16285eb011f5c4ea5e2c457cc7) +// Runtime types generated with workerd@1.20261110.1 2026-10-01 interface __BaseEnv_CloudflareBindings { AUTH_KV: KVNamespace; CACHE_KV: KVNamespace; @@ -15,7 +15,6 @@ interface __BaseEnv_CloudflareBindings { EXTENSION_PACKAGES_BASE_URL: "https://api.fossbilling.net/extensions/v2"; EXTENSION_ASSETS_PUBLIC_URL: "https://extensions-assets.fossbilling.net"; EXTENSIONS_RETENTION_MODE: "dry-run"; - EXTENSIONS_V2_EMAIL_PROVIDER: "mxroute"; M2M_ISSUER: "https://auth.fossbilling.net"; M2M_JWKS_URL: "https://auth.fossbilling.net/.well-known/jwks.json"; M2M_AUDIENCE: "https://api.fossbilling.net"; @@ -24,7 +23,8 @@ interface __BaseEnv_CloudflareBindings { GITHUB_APP_PRIVATE_KEY: string; ASSERTION_SIGNING_SECRET: string; EXTENSIONS_REVALIDATE_SECRET: string; - EXTENSIONS_V2_MXROUTE_PASSWORD: string; + EXTENSIONS_EMAIL_PROVIDER: string; + EXTENSIONS_EMAIL_PASSWORD: string; PREVIEW_GITHUB_BUDGET: DurableObjectNamespace; EXTENSIONS_FRONTEND: Fetcher /* extensions */; } @@ -40,7 +40,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } declare module "*.txt" { const value: string; @@ -956,39 +956,6 @@ interface Scheduler { interface SchedulerWaitOptions { signal?: AbortSignal; } -/** - * The **`ExtendableEvent`** interface extends the lifetime of the install and activate events dispatched on the global scope as part of the service worker lifecycle. This ensures that any functional events (like FetchEvent) are not dispatched until it upgrades database schemas and deletes the outdated cache entries. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent) - */ -declare abstract class ExtendableEvent extends Event { - /** - * The **`ExtendableEvent.waitUntil()`** method tells the event dispatcher that work is ongoing. It can also be used to detect whether that work was successful. In service workers, waitUntil() tells the browser that work is ongoing until the promise settles, and it shouldn't terminate the service worker if it wants that work to complete. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil) - */ - waitUntil(promise: Promise): void; -} -/** - * The **`CustomEvent`** interface can be used to attach custom data to an event generated by an application. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent) - */ -declare class CustomEvent extends Event { - constructor(type: string, init?: CustomEventCustomEventInit); - /** - * The read-only **`detail`** property of the CustomEvent interface returns any data passed when initializing the event. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail) - */ - get detail(): T; -} -interface CustomEventCustomEventInit { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; - detail?: any; -} /** * The **`Blob`** interface represents a blob, which is a file-like object of immutable, raw data; they can be read as text or binary data, or converted into a ReadableStream so its methods can be used for processing the data. * @@ -1398,6 +1365,39 @@ interface TextEncoderEncodeIntoResult { read: number; written: number; } +/** + * The **`ExtendableEvent`** interface extends the lifetime of the install and activate events dispatched on the global scope as part of the service worker lifecycle. This ensures that any functional events (like FetchEvent) are not dispatched until it upgrades database schemas and deletes the outdated cache entries. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent) + */ +declare abstract class ExtendableEvent extends Event { + /** + * The **`ExtendableEvent.waitUntil()`** method tells the event dispatcher that work is ongoing. It can also be used to detect whether that work was successful. In service workers, waitUntil() tells the browser that work is ongoing until the promise settles, and it shouldn't terminate the service worker if it wants that work to complete. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ExtendableEvent/waitUntil) + */ + waitUntil(promise: Promise): void; +} +/** + * The **`CustomEvent`** interface can be used to attach custom data to an event generated by an application. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent) + */ +declare class CustomEvent extends Event { + constructor(type: string, init?: CustomEventCustomEventInit); + /** + * The read-only **`detail`** property of the CustomEvent interface returns any data passed when initializing the event. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CustomEvent/detail) + */ + get detail(): T; +} +interface CustomEventCustomEventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + detail?: any; +} /** * The **`ErrorEvent`** interface represents events providing information related to errors in scripts or in files. * @@ -1513,6 +1513,40 @@ declare abstract class PromiseRejectionEvent extends Event { */ readonly reason: any; } +/** + * A **`CloseEvent`** is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent) + */ +declare class CloseEvent extends Event { + constructor(type: string, initializer?: CloseEventInit); + /** + * The **`code`** read-only property of the CloseEvent interface returns a WebSocket connection close code indicating the reason the connection was closed. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/code) + */ + readonly code: number; + /** + * The **`reason`** read-only property of the CloseEvent interface returns the WebSocket connection close reason the server gave for closing the connection; that is, a concise human-readable prose explanation for the closure. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/reason) + */ + readonly reason: string; + /** + * The **`wasClean`** read-only property of the CloseEvent interface returns true if the connection closed cleanly. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/wasClean) + */ + readonly wasClean: boolean; +} +interface CloseEventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; + code?: number; + reason?: string; + wasClean?: boolean; +} /** * The **`FormData`** interface provides a way to construct a set of key/value pairs representing form fields and their values, which can be sent using the fetch(), XMLHttpRequest.send() or navigator.sendBeacon() methods. It uses the same format a form would use if the encoding type were set to "multipart/form-data". * @@ -2772,7 +2806,7 @@ declare abstract class TailEvent extends ExtendableEvent { readonly traces: TraceItem[]; } interface TraceItem { - readonly event: (TraceItemFetchEventInfo | TraceItemJsRpcEventInfo | TraceItemConnectEventInfo | TraceItemScheduledEventInfo | TraceItemAlarmEventInfo | TraceItemQueueEventInfo | TraceItemEmailEventInfo | TraceItemTailEventInfo | TraceItemCustomEventInfo | TraceItemHibernatableWebSocketEventInfo) | null; + readonly event: (TraceItemFetchEventInfo | TraceItemJsRpcEventInfo | TraceItemConnectEventInfo | TraceItemScheduledEventInfo | TraceItemAlarmEventInfo | TraceItemOnHibernateEventInfo | TraceItemQueueEventInfo | TraceItemEmailEventInfo | TraceItemTailEventInfo | TraceItemCustomEventInfo | TraceItemHibernatableWebSocketEventInfo) | null; readonly eventTimestamp: number | null; readonly logs: TraceLog[]; readonly exceptions: TraceException[]; @@ -2834,6 +2868,9 @@ interface TraceItemFetchEventInfoResponse { interface TraceItemJsRpcEventInfo { readonly rpcMethod: string; } +interface TraceItemOnHibernateEventInfo { + readonly reason: "inactive" | "codeUpdated" | "system" | (string & {}); +} interface TraceItemHibernatableWebSocketEventInfo { readonly getWebSocketEvent: TraceItemHibernatableWebSocketEventInfoMessage | TraceItemHibernatableWebSocketEventInfoClose | TraceItemHibernatableWebSocketEventInfoError; } @@ -3221,40 +3258,6 @@ interface URLPatternResult { interface URLPatternOptions { ignoreCase?: boolean; } -/** - * A **`CloseEvent`** is sent to clients using WebSockets when the connection is closed. This is delivered to the listener indicated by the WebSocket object's onclose attribute. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent) - */ -declare class CloseEvent extends Event { - constructor(type: string, initializer?: CloseEventInit); - /** - * The **`code`** read-only property of the CloseEvent interface returns a WebSocket connection close code indicating the reason the connection was closed. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/code) - */ - readonly code: number; - /** - * The **`reason`** read-only property of the CloseEvent interface returns the WebSocket connection close reason the server gave for closing the connection; that is, a concise human-readable prose explanation for the closure. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/reason) - */ - readonly reason: string; - /** - * The **`wasClean`** read-only property of the CloseEvent interface returns true if the connection closed cleanly. - * - * [MDN Reference](https://developer.mozilla.org/docs/Web/API/CloseEvent/wasClean) - */ - readonly wasClean: boolean; -} -interface CloseEventInit { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; - code?: number; - reason?: string; - wasClean?: boolean; -} type WebSocketEventMap = { close: CloseEvent; message: MessageEvent; @@ -3678,6 +3681,7 @@ interface Tracing { startActiveSpan(name: string, callback: (span: Span, ...args: A) => T, ...args: A): T; startSpan(name: string): Span; getActiveSpan(): Span | undefined; + withActiveSpan(span: Span, callback: (...args: A) => T, ...args: A): T; Span: typeof Span; } declare abstract class Span { @@ -14241,6 +14245,12 @@ interface JsonWebKeyWithKid extends JsonWebKey { } interface RateLimitOptions { key: string; + /** + * Amount to add to the counter for this call. Must be a positive integer; + * defaults to `1`. The call is allowed while the counter is below the limit, + * even if `delta` takes it past the limit. + */ + delta?: number; } interface RateLimitOutcome { success: boolean; @@ -15394,6 +15404,10 @@ declare namespace TailStream { readonly type: "alarm"; readonly scheduledTime: Date; } + interface OnHibernateEventInfo { + readonly type: "onHibernate"; + readonly reason: "inactive" | "codeUpdated" | "system" | (string & {}); + } interface QueueEventInfo { readonly type: "queue"; readonly queueName: string; @@ -15458,7 +15472,7 @@ declare namespace TailStream { readonly scriptTags?: string[]; readonly scriptVersion?: ScriptVersion; readonly preview?: TracePreviewInfo; - readonly info: FetchEventInfo | ConnectEventInfo | JsRpcEventInfo | ScheduledEventInfo | AlarmEventInfo | QueueEventInfo | EmailEventInfo | TraceEventInfo | HibernatableWebSocketEventInfo | CustomEventInfo; + readonly info: FetchEventInfo | ConnectEventInfo | JsRpcEventInfo | ScheduledEventInfo | AlarmEventInfo | OnHibernateEventInfo | QueueEventInfo | EmailEventInfo | TraceEventInfo | HibernatableWebSocketEventInfo | CustomEventInfo; } interface Outcome { readonly type: "outcome"; diff --git a/services/api/wrangler.jsonc b/services/api/wrangler.jsonc index 465eab43..66303011 100644 --- a/services/api/wrangler.jsonc +++ b/services/api/wrangler.jsonc @@ -29,7 +29,6 @@ "EXTENSION_PACKAGES_BASE_URL": "https://api.fossbilling.net/extensions/v2", "EXTENSION_ASSETS_PUBLIC_URL": "https://extensions-assets.fossbilling.net", "EXTENSIONS_RETENTION_MODE": "dry-run", - "EXTENSIONS_V2_EMAIL_PROVIDER": "mxroute", "M2M_ISSUER": "https://auth.fossbilling.net", "M2M_JWKS_URL": "https://auth.fossbilling.net/.well-known/jwks.json", "M2M_AUDIENCE": "https://api.fossbilling.net" @@ -123,7 +122,7 @@ "vars": { "ENVIRONMENT": "preview", "EXTENSION_PACKAGES_MODE": "disabled", - "EXTENSIONS_V2_EMAIL_PROVIDER": "disabled", + "EXTENSIONS_EMAIL_PROVIDER": "disabled", "EXTENSIONS_RETENTION_MODE": "dry-run" }, "durable_objects": {