v0.24.0
Minor Changes
-
Admin direct gifts: admins can gift bonus credits straight to an existing user by email from the dashboard's admin Credits page (renamed from "Credit Requests"). Each gift is recorded as an already-approved
kind='gift'row incredit_requests— it shows tagged in the admin list and as "Gift from the DeepSpace team" in the recipient's Billing history, and the grant shares the same expiring-bonus semantics as approvals and promo redemptions. The credits dialogs were also hardened: API errors always surface curated copy (raw server/transport messages never render), and Esc/backdrop can't dismiss a dialog while its request is in flight. -
Managed knowledge base: an app can declare one
[[ai_search]]binding withinstance_name = "auto"and get an isolated Cloudflare AI Search instance, provisioned on deploy and torn down on undeploy like any other"auto"resource. The newknowledge(env)helper ondeepspace/workeranddeepspace/serverexposesadd/list/remove/search, plusscoped({ folder })for a handle whose folder a caller cannot widen — safe to hand to an agent as a tool. Folders are recursive prefix scopes; oversized text files are split at UTF-8-safe boundaries and oversized binaries are refused rather than corrupted.The resolved binding is control-plane metadata only: it is stripped before the Workers-for-Platforms upload, so no Cloudflare token, instance id, or
ai_searchbinding ever reaches the customer Worker, andai_search_namespacesis refused outright. Every operation is authenticated per app and charged in the API worker, so calling the route directly is billed exactly like the helper. Search is charged up front, ingestion reserves and settles against the real indexed-token count, and storage is sampled daily — all at the existing 10% platform binding markup (third-party integrations are unchanged at 30%) and surfaced in the dashboard's Binding Usage card. Available on every plan. -
Add
--viewport WIDTHxHEIGHTtodeepspace test screenshot. The helper normalizes the dimensions for Playwright's installed Chromium browser, enabling reliable mobile captures without selecting a device profile backed by an uninstalled browser. -
Promo codes: admins (and Beacon, via app identity + owner JWT) can bulk-mint single-use codes that grant expiring bonus credits — idempotent per (campaign, email) when a campaign is given — and users redeem them from a new "Redeem Code" card on the dashboard Billing page. Includes an admin mint dialog on the Credits page and a rate-limited, race-safe redeem endpoint. Each code also carries a clickable claim link (dashboard
/claim/<token>): signed-out recipients see the offer, sign in, and the credits auto-apply — the grant attaches to the claim, not to signup, so pre-existing accounts and any click/signup order work. -
Remove the
workspace:defaultshared scope — the one platform-wide Durable Object that held "cross-app shared business data" for every app. TheWORKSPACE_SCHEMASexport (deepspace/schema), theworkspaceentry inGLOBAL_DO_TYPES, and the platform worker's acceptance ofworkspace:defaultas a global scope id are gone; the platform now answers400 Invalid global scopeIdfor it. Cross-app data is an app's owndir:{appId}room or aconv:{convId}room, never a single DO shared by all apps. Team-scoped RBAC (teamField/ ateam_memberscollection) is unchanged and remains available to any app's own rooms.
Patch Changes
-
Stop gating source selection and transfers on unrelated local Git state. Initial GitHub claims and direct GitHub repository changes now require only remote reachability, matching deploys that intentionally ship dirty and unpushed bytes; provider transfers still verify the authoritative refs and release lineage before flipping authority.
-
Stop credit-request approvals from resurrecting a user's expired bonus credits: when the existing bonus allocation has already expired, approving a grant now resets the balance to just the newly granted credits instead of adding to the stale amount and refreshing its expiry.
-
deepspace app updatenow reports the secure-room-boundaries blocker wherever the legacy room proxy lives, not only atsrc/server/realtime-routes.ts. An app that had moved its WebSocket proxy and still forwarded identity in the URL passedapp updateclean, then reached the room as anonymous on every connection — RBAC filtered every row and signed-in users saw an empty app. The blocker scan is keyed on content (a file that forwards a request and setsuserId/role… on its URL) instead of on the file path; the auto-rewrite is unchanged. Rooms also log a warning when a connection carries?userId=with nox-user-idheader, so the pre-0.19 proxy shape names itself inwrangler tail. -
Return a stable duplicate result when an update violates a collection's
uniqueOnconstraint. -
Fixes from the v0.23.2 production AX pass. Server: the
user.listassistant tool now returns exactly what the socket roster gives the caller (admins full rows through the read policy; everyone else the public identity projection, honouringread: falseandroster: 'read-policy'; app actions keep full rows) — it used to hand every member every user's email; the public identity projection now carrieslastSeenAt, sousePresence()works for non-admins (it was always offline for everyone but the owner);timestampTriggerwrites canonical epoch seconds on astorage: 'number'date/datetime column instead of an ISO string that stored the year or milliseconds inconsistent with explicit writes; an empty string written to a text column is stored as''(it was folded into NULL, so the field read back absent and adefault: ''never materialized);whereonrecords.query/deleteWhererefuses arrays and non-objects at the one shared chokepoint;deleteWhereauthorizes with delete permission independently of read permission; the client'swritableFieldsrefusal names the role and field again. CLI:deploystates on both surfaces that a rename does not carry the display name (src/constants.tsandwrangler.toml [vars] APP_NAME) and the--renamesuccess envelope carriesrenamedFrom/staleDisplayName;collaborators adddiscloses that a collaborator can read and change every secret (grantsin--json); the false "attributed to the app's owner" warning on collaborator deploys is gone (the ledger records the collaborator);app source githubremoves a stalespacegit remote (spaceRemotein--json), refuses stale import writes after an authority flip, rechecks GitHub after import, and preserves a successful flip when local remote reconciliation fails;push/pull/cloneshare one GitHub-source refusal withrepositoryandappIdas fields (apiFetchnow keeps every server-provided field onApiError.details);push's uninitialized-scaffold refusal isactionRequired(exit 2) likedeploy's;releases/status/activityname a GitHub-source release's source instead of "(no source recorded)"; a GitHub-source deploy reports its branch and whether the tree was dirty (both surfaces);app updateis now a successful read-only guide tied to the running CLI version: it reads only explicit app manifests and never rewrites source, stamps migrations, runs an installer, scans unrelated packages, imposes DeepSpace Git transport checks, or treats outstanding work as a command failure; local/file/workspace/VCS SDK specs reportdependency_unverifiedinstead of a false aligned state, and malformed migration ledgers refuse withinvalid_migration_manifest;test run --jsonroutes the Playwright dependency preflight to stderr through the same chokepoint as the suite and says up front when it will runapt-get; theunknown_suiterefusal mentions--grepand single-spec paths;test accounts createno longer prints the password (it is saved locally;list --revealshows it);ActionToolsgainsdeleteWhere(template + upgrade guidance). Deploy locking no longer reclaims stale paths automatically, and release callbacks delete only the lock they own. Scaffolder:create-deepspace@Xpinsdeepspaceto exactlyX(a caret let a pinned scaffolder install the newest SDK) and prints the SDK version it installed.Second round (wave 2 of the pass — recovery, upgrade, observability). CLI: a present-but-malformed
DEEPSPACE_APP_IDis refused asinvalid_app_idat the one id resolver (it was reported as "no app id" and the offeredapp initorphaned the app;app initnow refuses to overwrite it without--new-id); the "no app dir" vs "app dir without an id" states get one code each (not_in_app_repo/app_not_initialized) acrossdeploy,secrets,test,dev;secretsandauth loginrefusals go through the shared envelope (not_authenticatedwith its action,network_errornaming the service and env var,file_not_found,invalid_credentials);not_authenticatednames the selected plane vs the plane holding a session (DEEPSPACE_ENV/DEEPSPACE_AUTH_URL) and the headless login form;push/pull/deployrefusemerge_in_progressmid-merge instead of pushing the pre-merge commit;deploytakes a local.deepspace/deploy.lock(deploy_in_progressnames the other run) so two deploys in one directory no longer race ondist/,release_in_progressexplains itself and carries a retry action,forbiddennames the signed-in account and the app, and after the edge confirms a releasedeploywakes the worker once;undeploy's confirmation says what is destroyed (the app's Durable Objects: records, messages, canvas state, cron history — the old sentence said "data stays") and what stays (secrets, registration), names the app, and a second undeploy reportsalreadyUndeployed;logson a never-deployed app refusesapp_not_deployed;app init --jsonreports the plane asenv(the wrangler slot iswranglerEnv);test accounts list --jsonprints passwords only with--reveal;status --helpnamesenv/services;integrations invokerefuses a paid call outside a terminal or under--jsonwithout--yes(cost_confirmation_required; the interactive prompt defaults to No) andintegrations infosynthesizes an example body from the schema's required keys;app updatereports guidance for the build-injected app id and action-route bearer guard without mutating the checkout;app init'sapp_not_registeredrefusal ships the--new-idaction;workspace landpins its follow-up to the surviving primary checkout instead of the managed worktree it just removed. Server: a cron schedule arms on the worker's first request (armCronRoom, wired in the template) instead of waiting for its Durable Object to be fetched by a visitor, and rejected or non-2xx wake attempts remain retryable; cron and job runs log one structured line each; the template'sapp.onErrorlogs a thrown error's message (Hono's default rendered only the stack frames, sodeepspace logsnever carried it). Platform (ships with the next worker deploy): a created-never-written secrets config answers{}instead of "Project DEK not found"; non-request log invocations (alarms) are labelled with their entrypoint; the integrations catalog discloses the customer price after markup and currency conversion (nullfor meteredper_actual_costendpoints,variesWithInputwhere multipliers apply), andintegrations list/info/the consent prompt label an input-dependent figure as a base rate that can move lower or higher instead of falsely calling it a floor. Template: the action route logs[action] <name> caller=<userId>(the platform request log carries no user), with upgrade guidance for existing apps.