Skip to main content

security-delete

Security delete API​

The Security Delete API permanently deletes legacy Walrus Blob objects that were tracked in Walrus Memory's old-V1 database. The server selects and verifies the objects, builds and sponsors the Sui transaction, and executes it. The client only authenticates its wallet and signs the returned TransactionData.

This API never enrolls caller-supplied blobs into the legacy tracking set and never deletes vector_entries directly. Existing recall cleanup removes stale index rows after the corresponding Walrus object is gone.

Availability and feature flags​

The new API is available only when both flags are enabled:

Source: api/security-delete.md
ENABLE_MEMORY_DELETION=true
ENABLE_SECURITY_DELETE=true

The master flag without the selector exposes only the old deletion API. The selector without the master flag is invalid and prevents server startup. No configuration exposes both destructive API families simultaneously.

In every valid running configuration where the route pair is not enabled, every endpoint in this guide returns the closed 404 FEATURE_DISABLED response. This feature gate runs before authentication, query parsing, and body extraction, so an unauthenticated or malformed request does not reveal a disabled deployment.

Enabling the API also requires LEGACY_DB_URL, SUI_GRPC_URL, WALRUS_PACKAGE_ID, WALRUS_SYSTEM_OBJECT_ID, DELETION_TOKEN_SECRET, and a base64-encoded 32-byte SPONSOR_PRIVATE_KEY. Configuration changes require a restart or rolling deployment.

The API supports browser requests from any origin. Cross-origin preflight requests allow GET, POST, and DELETE, plus the Content-Type and Authorization headers used by the flow below. Authentication and all other authorization checks still apply to cross-origin callers.

The background selectors are independent of route exposure: DELETION_OBJECT_RESOLVER_ENABLED might run with both route flags off, and DELETION_RECONCILER_ENABLED might reconcile existing batches while routes are off. Either job still requires the legacy database and Sui gRPC configuration; the reconciler additionally requires the sponsor key. Enabling a background job does not make any HTTP endpoint reachable.

Authentication​

Authentication uses a single-use wallet challenge followed by a short-lived Bearer token. Addresses are canonicalized to lowercase, 32-byte Sui hex form before they are stored or compared.

1. Request a challenge​

Source: api/security-delete.md
POST /api/security-delete-auth/challenge
Content-Type: application/json

{"address":"0xabc"}
Source: api/security-delete.md
{
"challengeId": "31db510d-85f3-42ad-875a-0c14df36d40e",
"challenge": "MemWal security deletion auth\naddress: 0x0000000000000000000000000000000000000000000000000000000000000abc\nnonce: 31db510d-85f3-42ad-875a-0c14df36d40e\nexpires: 1783760000",
"expiresInSecs": 300
}

Sign the exact UTF-8 challenge string as a Sui personal message. Do not reconstruct or normalize it on the client.

2. Verify the challenge​

Source: api/security-delete.md
POST /api/security-delete-auth/verify
Content-Type: application/json

{
"challengeId": "31db510d-85f3-42ad-875a-0c14df36d40e",
"address": "0xabc",
"signature": "<base64 Sui user signature>"
}
Source: api/security-delete.md
{
"token": "<security-delete bearer token>",
"expiresInSecs": 2700
}

challengeId is required and opaque. Challenges for the same wallet are independent, but each ID can be consumed only once. Verification supports the native Sui signature schemes and zkLogin through the fullnode's live onchain JWK set.

Send the token on every endpoint below:

Source: api/security-delete.md
Authorization: Bearer <token>

Malformed, tampered, and expired tokens intentionally share AUTH_TOKEN_EXPIRED.

List tracked blobs​

Source: api/security-delete.md
GET /api/security-deletable-blobs?state=deletable&limit=100&cursor=<opaque>
Authorization: Bearer <token>
Source: api/security-delete.md
{
"items": [
{
"blobId": "X4...",
"objectId": "0x123...",
"createdAt": "2026-07-11T08:15:30.123456+00:00",
"state": "deletable"
}
],
"counts": {
"total": 12,
"deletable": 8,
"deleting": 2,
"deleted": 1,
"deletedExternal": 0,
"notOwner": 1,
"expired": 0
},
"limits": { "deleteBatchMax": 900 },
"nextCursor": "<opaque-or-null>"
}

The default state is deletable. state accepts one or more comma-separated values from:

Source: api/security-delete.md
deletable,deleting,deleted,deleted_external,not_owner,expired

Results use keyset ordering by (createdAt, blobId). Cursors are opaque and versioned; an invalid cursor never silently restarts at the first page. limit must be between 1 and 200. Counts always cover all states for the authenticated owner, regardless of the current filter.

limits.deleteBatchMax is authoritative. It defaults to and is hard-capped at 900 because the measured 1,000-object Sui transaction exceeds the protocol's 128-KiB transaction-size limit.

Prepare a deletion​

Delete up to 900 currently deletable blobs selected by the backend:

Source: api/security-delete.md
POST /api/security-deletions
Authorization: Bearer <token>
Content-Type: application/json

{"mode":"all"}

Or request an exact tracked selection:

Source: api/security-delete.md
{
"mode": "selection",
"blobIds": ["blob-a", "blob-b"]
}

Successful preparation returns unsigned transaction bytes for the wallet:

Source: api/security-delete.md
{
"batchId": "ce77795f-ecf7-4a76-806f-abef834152b0",
"txBytes": "<base64 BCS TransactionData>",
"included": 2,
"excluded": [
{ "blobId": "blob-c", "reason": "expired" }
],
"expiresAt": "epoch:931"
}

Treat expiresAt as an informational, opaque expiration marker. Sign the decoded txBytes as Sui transaction data (not as a personal message) and submit that signature to the next endpoint.

If nothing remains after validation, preparation succeeds without a batch:

Source: api/security-delete.md
{
"batchId": null,
"txBytes": null,
"included": 0,
"excluded": [],
"expiresAt": null
}

Preparation is strict:

  • mode must be exactly all or selection.
  • all must not include blobIds.
  • selection must contain 1–900 unique, non-empty IDs.
  • Every selected ID must already exist in the authenticated owner's legacy tracking set and be deletable. One conflict rejects the entire selection; no row is partially claimed and no untracked blob is inserted.
  • Objects are re-read from Sui before the transaction is built. Missing, transferred, and near-expiry objects are removed from the batch and reported as already_deleted, not_owner, or expired.
  • The returned transaction reflects the final included set. Its input ordering is persisted so later Sui failures can be mapped safely back to blob IDs.

Up to 16 batches per owner are active by default. The cap check and claim occur under one short owner-scoped database lock, so concurrent prepares cannot race past the cap. Independent prepared batches can be signed and submitted in parallel afterward.

Submit a signed batch​

Source: api/security-delete.md
POST /api/security-deletions/ce77795f-ecf7-4a76-806f-abef834152b0/submit
Authorization: Bearer <token>
Content-Type: application/json

{"signature":"<base64 Sui transaction signature>"}
Source: api/security-delete.md
{
"state": "completed",
"deleted": 2,
"digest": "8v..."
}

The server verifies the user signature against the stored bytes, atomically claims execution, adds the sponsor signature, executes the transaction, checks the returned digest and effects, then marks only that batch generation as deleted.

If execution returns a timeout, rate-limit response, or another outcome whose onchain result is unknown, the API returns RPC_UNAVAILABLE and deliberately leaves the batch in executing. Do not immediately prepare replacements for the same blobs. Poll the status endpoint; the reconciler confirms the digest and finalize or release the batch.

Committed failures are resolved conservatively. When Sui identifies a culprit input, Walrus Memory rechecks that object, terminalizes only confirmed missing or transferred blobs, and releases the remaining rows. Unknown failure shapes trigger a full batch diff. Every write still includes the expected batchId, so a late result from an older batch cannot mutate rows reclaimed by a newer batch.

Get batch status​

Source: api/security-delete.md
GET /api/security-deletions/ce77795f-ecf7-4a76-806f-abef834152b0
Authorization: Bearer <token>
Source: api/security-delete.md
{
"state": "executing",
"blobCount": 2,
"digest": "8v...",
"resolvedAt": null
}

Possible states are awaiting_signature, executing, completed, failed, and rolled_back. A batch owned by another wallet is indistinguishable from an unknown batch and returns BATCH_NOT_FOUND.

Cancel a prepared batch​

Source: api/security-delete.md
DELETE /api/security-deletions/ce77795f-ecf7-4a76-806f-abef834152b0
Authorization: Bearer <token>

Cancellation is accepted only while the batch is awaiting_signature. It atomically changes the batch to rolled_back and releases only rows still owned by that batch generation. The response uses the same shape as batch status. Repeating the cancellation returns BATCH_ALREADY_RESOLVED.

Validation and runtime limits​

BoundaryLimit
Challenge body4 KiB
Verify body16 KiB
Prepare body256 KiB
Submit body16 KiB
List page1–200 items
Delete batch1–900 included blobs
Active batches per owner16 by default
Challenge lifetime300 seconds
Bearer lifetime2,700 seconds by default
Auth challenge/verify rate20 requests per IP per 60 seconds
Prepare rate10 requests per owner per 60 seconds

Malformed JSON, oversized bodies, malformed UUID path parameters, malformed query encoding, unknown filters, invalid cursors, and invalid selections all use the same closed INVALID_REQUEST envelope. Client-supplied values are not echoed in error messages.

Error envelope​

Every error has this shape:

Source: api/security-delete.md
{
"error": {
"code": "BATCH_CONFLICT",
"retriable": false,
"action": "REFETCH",
"message": "Deletion selection conflicts with current state",
"details": { "conflictingBlobIds": ["blob-b"] }
}
}

Clients should branch on action, use code for diagnostics, and treat message as display text.

CodeHTTPRetriableActionTypical meaning
AUTH_CHALLENGE_EXPIRED401noREAUTHChallenge expired, was consumed, or does not match the wallet
AUTH_INVALID_SIGNATURE401noNONEChallenge signature is invalid
AUTH_TOKEN_EXPIRED401noREAUTHBearer is missing, malformed, tampered, or expired
INVALID_REQUEST400noNONERequest shape, filter, cursor, path, or selection is invalid
ACTIVE_BATCH_LIMIT409noREFETCHOwner has reached the active-batch cap; details include activeBatchIds
BATCH_CONFLICT409noREFETCHOne or more selected blobs are missing or no longer deletable
NOTHING_TO_DELETE200*noREFETCHRepresented by the successful null-batch prepare response, not an error response
BATCH_NOT_FOUND404noREFETCHBatch does not exist for the authenticated owner
BATCH_ALREADY_RESOLVED409noREFETCHBatch is executing or terminal; details include its state
BATCH_EXPIRED410noRE_PREPAREPrepared batch was rolled back or expired
TX_EXECUTION_FAILED502noRE_PREPARESui committed a failure; details might include evicted blob IDs
SPONSOR_FUNDS_UNAVAILABLE503yesRETRY_AFTERSponsor address balance cannot fund gas; do not re-prepare the unchanged blob pool
INVALID_SIGNATURE400noNONESubmit signature does not verify against stored transaction bytes
RATE_LIMITED429yesRETRY_AFTERRetry after details.retryAfterSecs
RPC_UNAVAILABLE503yesRETRY_AFTERSui outcome is unavailable or uncertain; poll status when submit reached execution
INTERNAL_ERROR500yesRETRY_AFTERSanitized server failure; details contain only a traceId
FEATURE_DISABLED404noNONEMaster flag or security-delete selector is off

* NOTHING_TO_DELETE is a semantic code used by the contract documentation; the shipped HTTP response is the 200 prepare success shape with batchId: null.

End-to-end example​

Source: api/security-delete.md
# 1. Request and sign the returned personal-message challenge in the wallet.
$ curl -sS "$MEMWAL_URL/api/security-delete-auth/challenge" \
-H 'content-type: application/json' \
-d '{"address":"0xabc"}'

# 2. Exchange challengeId + exact-message signature for a Bearer.
$ curl -sS "$MEMWAL_URL/api/security-delete-auth/verify" \
-H 'content-type: application/json' \
-d '{"challengeId":"<id>","address":"0xabc","signature":"<personal-signature>"}'

# 3. Prepare one backend-selected batch.
$ curl -sS "$MEMWAL_URL/api/security-deletions" \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"mode":"all"}'

# 4. Sign decoded txBytes as Sui TransactionData, then submit.
$ curl -sS "$MEMWAL_URL/api/security-deletions/$BATCH_ID/submit" \
-H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"signature":"<transaction-signature>"}'

# 5. Recover after a dropped/ambiguous submit response.
$ curl -sS "$MEMWAL_URL/api/security-deletions/$BATCH_ID" \
-H "authorization: Bearer $TOKEN"

Concurrency and safety guarantees​

  • Owner identity always comes from the verified Bearer, never a request body.
  • Selection claims update existing legacy tracking rows only and are all-or-nothing.
  • Active-cap checking, batch creation, and row claiming share one transaction under an owner-scoped advisory lock.
  • Every batch transition compares the expected state.
  • Every mutation of a claimed blob compares owner, blob ID, expected state, and expected batch ID.
  • Cancel, failure rollback, and successful finalization are atomic database transactions.
  • Transaction input-to-blob ordering is persisted before signing.
  • Sponsor signatures are generated only at submit time and are never returned or stored.
  • Transport failures with unknown execution outcomes remain executing until status reconciliation proves the result.

These rules prevent partial selection, active-cap races, arbitrary blob enrollment, replay between batches, and stale asynchronous results overwriting a newer claim.