feat(drive): expose authorized typed Workspace Drive API

T-722: reuse T-721 storage/CAS/receipts with clone-bound transaction authorization against attached Server authority. Add migration 84 audited Worker read_only/read_write grants and removal/deletion fencing, member-facing ordinary access, owner-controlled grant settings, Workspace-bound decimal entry references, bounded text/binary chunks, streamed HTTP receive/download with integrity and safe headers, typed errors and generated Rust/OpenAPI/TypeScript contracts.

Validation: root cargo check; server-api 83 default / 91 typescript tests; FeatureStorage 16; Drive 31 unit + 12 integration; Workspace Server 691 library + 11 binary tests (--test-threads=4), including 9 real DB/LocalFileSystem HTTP tests for two Workers/non-owner usage, revoke during reception, concurrent CAS, response-loss receipts, chunk expiry and safe binary media. cargo fmt --all -- --check and git diff --check HEAD passed. Existing OpenAPI schemas/routes semantically unchanged; dependent TS digest projections regenerated.
This commit is contained in:
2026-10-09 00:02:09 +09:00
parent 9da79ab9f0
commit b5ea162f83
25 changed files with 6416 additions and 16 deletions
+4
View File
@@ -55,3 +55,7 @@ required-features = ["typescript"]
[[example]]
name = "generate_repository_access_types"
required-features = ["typescript"]
[[example]]
name = "generate_drive_api_types"
required-features = ["typescript"]
@@ -0,0 +1,6 @@
fn main() {
print!(
"{}",
server_api::normalized_typescript(server_api::drive_api_typescript())
);
}
+902
View File
@@ -0,0 +1,902 @@
//! Workspace Drive HTTP contracts. Entries are logical, workspace-bound resources,
//! never host paths. Worker identity and grants are resolved by the service from
//! trusted request context, not supplied by ordinary operation DTOs.
use schemars::JsonSchema;
use serde::{Deserialize, Deserializer, Serialize};
pub const DRIVE_TEXT_MAX_BYTES: usize = 64 * 1024;
pub const DRIVE_CHUNK_MAX_BYTES: u32 = 64 * 1024;
pub const DRIVE_PAGE_MAX_LIMIT: u32 = 200;
pub const DRIVE_SEARCH_MAX_LIMIT: u32 = 128;
pub const DRIVE_FILE_MAX_BYTES: u32 = 16 * 1024 * 1024;
fn decimal<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
let value = String::deserialize(deserializer)?;
if !canonical_positive_decimal(&value) {
return Err(serde::de::Error::custom(
"expected a canonical positive decimal string",
));
}
Ok(value)
}
fn optional_decimal<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<String>, D::Error> {
let value = Option::<String>::deserialize(deserializer)?;
if value
.as_deref()
.is_some_and(|value| !canonical_positive_decimal(value))
{
return Err(serde::de::Error::custom(
"expected a canonical positive decimal string",
));
}
Ok(value)
}
fn canonical_positive_decimal(value: &str) -> bool {
value
.as_bytes()
.first()
.is_some_and(|first| matches!(first, b'1'..=b'9'))
&& value.bytes().all(|byte| byte.is_ascii_digit())
&& value.parse::<i64>().is_ok_and(|value| value > 0)
}
fn bounded_text<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
let value = String::deserialize(deserializer)?;
if value.len() > DRIVE_TEXT_MAX_BYTES {
return Err(serde::de::Error::custom("Drive text exceeds 64 KiB"));
}
Ok(value)
}
fn bounded_chunk<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u32, D::Error> {
let value = u32::deserialize(deserializer)?;
if value == 0 || value > DRIVE_CHUNK_MAX_BYTES {
return Err(serde::de::Error::custom(
"Drive read size must be between 1 and 65536 bytes",
));
}
Ok(value)
}
fn optional_limit<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<u32>, D::Error> {
let value = Option::<u32>::deserialize(deserializer)?;
if value.is_some_and(|value| value == 0 || value > DRIVE_PAGE_MAX_LIMIT) {
return Err(serde::de::Error::custom(
"Drive page limit must be between 1 and 200",
));
}
Ok(value)
}
const fn default_read_size() -> u32 {
DRIVE_CHUNK_MAX_BYTES
}
fn optional_search_limit<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<u32>, D::Error> {
let value = Option::<u32>::deserialize(deserializer)?;
if value.is_some_and(|value| value == 0 || value > DRIVE_SEARCH_MAX_LIMIT) {
return Err(serde::de::Error::custom(
"Drive search limit must be between 1 and 128",
));
}
Ok(value)
}
/// Numeric node IDs alone are not public references: the referenced Workspace
/// must match the route Workspace before the service resolves the node.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveEntryRef {
pub workspace_id: String,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub node_id: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum DriveEntryKind {
Folder,
File,
}
/// Metadata only: content is returned through bounded text or binary routes.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveEntry {
pub entry: DriveEntryRef,
pub parent: Option<DriveEntryRef>,
pub name: String,
pub kind: DriveEntryKind,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub revision: String,
/// File length in bytes; absent for folders.
#[schemars(range(min = 0, max = 16_777_216))]
pub size: Option<u32>,
pub content_type: Option<String>,
pub updated_by: String,
/// Timestamp projected from storage's updated_at_ms; not a creation time.
pub updated_at: String,
/// Stable relative authenticated URL in this same Workspace. No bearer token
/// or cross-Workspace link is embedded in this metadata.
pub latest_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveEntryQuery {
pub entry_workspace_id: String,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveListQuery {
pub entry_workspace_id: String,
/// Folder to list.
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub id: String,
#[serde(default, deserialize_with = "optional_limit")]
#[schemars(range(min = 1, max = 200))]
pub limit: Option<u32>,
/// Opaque service-issued page cursor, not a bare node ID.
#[serde(default)]
pub after: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveSearchQuery {
pub query: String,
/// Search file text as well as names only when explicitly requested.
#[serde(default)]
pub include_text: bool,
#[serde(default, deserialize_with = "optional_search_limit")]
#[schemars(range(min = 1, max = 128))]
pub limit: Option<u32>,
#[serde(default)]
pub after: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveListResponse {
pub entries: Vec<DriveEntry>,
pub next_after: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveReadTextQuery {
pub entry_workspace_id: String,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub id: String,
#[serde(default = "default_read_size", deserialize_with = "bounded_chunk")]
#[schemars(range(min = 1, max = 65536))]
pub max_bytes: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveReadTextResponse {
pub entry: DriveEntry,
#[serde(deserialize_with = "bounded_text")]
pub text: String,
pub truncated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveReadChunkQuery {
pub entry_workspace_id: String,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub id: String,
/// Bind a multi-chunk read to the same file revision.
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub expected_revision: String,
#[schemars(range(min = 0, max = 16_777_216))]
pub offset: u32,
#[serde(default = "default_read_size", deserialize_with = "bounded_chunk")]
#[schemars(range(min = 1, max = 65536))]
pub length: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveDownloadQuery {
pub entry_workspace_id: String,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub id: String,
#[serde(default, deserialize_with = "optional_decimal")]
pub expected_revision: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveMutationRequest {
pub request_id: String,
pub mutation: DriveMutation,
}
/// JSON can mutate bounded UTF-8 TEXT only. Arbitrary file bytes use drive_upload.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "operation", rename_all = "snake_case", deny_unknown_fields)]
pub enum DriveMutation {
CreateFolder {
parent: DriveEntryRef,
name: String,
},
CreateText {
parent: DriveEntryRef,
name: String,
#[serde(deserialize_with = "bounded_text")]
text: String,
content_type: String,
},
UpdateText {
id: DriveEntryRef,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
expected_revision: String,
#[serde(deserialize_with = "bounded_text")]
text: String,
content_type: String,
},
Relocate {
id: DriveEntryRef,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
expected_revision: String,
parent: DriveEntryRef,
name: String,
},
Delete {
id: DriveEntryRef,
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
expected_revision: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveMutationResponse {
pub request_id: String,
/// A deleted entry has no live metadata.
pub entry: Option<DriveEntry>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum DriveUploadOperation {
Create,
Update,
}
/// Flat query metadata accompanies the binary body. The service rejects invalid
/// combinations: create requires parent_id/name and forbids id/expected_revision;
/// update requires id/expected_revision and forbids parent_id/name. Both targets
/// are bound by entry_workspace_id; declared size and SHA256 must match the body.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveUploadQuery {
pub operation: DriveUploadOperation,
pub request_id: String,
pub entry_workspace_id: String,
#[serde(default, deserialize_with = "optional_decimal")]
pub parent_id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default, deserialize_with = "optional_decimal")]
pub id: Option<String>,
#[serde(default, deserialize_with = "optional_decimal")]
pub expected_revision: Option<String>,
pub content_type: String,
/// Values above the file limit still decode as u32 so the service returns
/// typed Limit rather than a generic request-decoding error.
#[schemars(range(min = 0, max = 16_777_216))]
pub size: u32,
pub sha256: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum DriveRequestState {
/// No commit record exists at the observation snapshot. An in-flight request
/// can still commit afterward; this is not proof of a final failed outcome.
Uncommitted,
Committed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveRequestStatusResponse {
pub request_id: String,
pub state: DriveRequestState,
pub response: Option<DriveMutationResponse>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum DriveAccess {
ReadOnly,
ReadWrite,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveGrantCreateRequest {
pub runtime_id: String,
pub worker_id: String,
pub access: DriveAccess,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveGrantResponse {
#[serde(deserialize_with = "decimal")]
#[schemars(regex(pattern = "^[1-9][0-9]*$"))]
pub grant_id: String,
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub access: DriveAccess,
pub revoked: bool,
pub created_by: String,
pub created_at: String,
pub revoked_by: Option<String>,
pub revoked_at: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveGrantListQuery {
#[serde(default, deserialize_with = "optional_limit")]
#[schemars(range(min = 1, max = 200))]
pub limit: Option<u32>,
/// Canonical grant ID cursor within this Workspace.
#[serde(default, deserialize_with = "optional_decimal")]
pub after: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveGrantListResponse {
pub grants: Vec<DriveGrantResponse>,
pub next_after: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum DriveApiErrorCode {
Denied,
NotFound,
Conflict,
Invalid,
Limit,
StorageUnavailable,
OutcomeUnknown,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum DriveFailureClassification {
NotCommitted,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct DriveApiError {
pub code: DriveApiErrorCode,
pub classification: DriveFailureClassification,
pub message: String,
}
impl DriveApiError {
/// Never include provider errors, paths, SQL, or caller-supplied messages.
pub fn new(code: DriveApiErrorCode) -> Self {
use DriveApiErrorCode::*;
Self {
code,
classification: if code == OutcomeUnknown {
DriveFailureClassification::Unknown
} else {
DriveFailureClassification::NotCommitted
},
message: match code {
Denied => "Drive access denied",
NotFound => "Drive resource not found",
Conflict => "Drive revision or name conflict",
Invalid => "Invalid Drive request",
Limit => "Drive request exceeds a limit",
StorageUnavailable => "Drive storage unavailable",
OutcomeUnknown => "Drive mutation outcome unknown",
}
.into(),
}
}
}
impl api_macros::HttpError for DriveApiError {
fn status_code(&self) -> u16 {
use DriveApiErrorCode::*;
match self.code {
Denied => 403,
NotFound => 404,
Conflict => 409,
Invalid => 400,
Limit => 413,
StorageUnavailable | OutcomeUnknown => 503,
}
}
}
impl api_macros::HttpRequestError for DriveApiError {
fn from_request_rejection(status: u16, _message: String) -> Self {
Self::new(if status == 413 {
DriveApiErrorCode::Limit
} else {
DriveApiErrorCode::Invalid
})
}
}
impl api_macros::openapi::OpenApiSchema for DriveApiError {}
impl api_macros::openapi::OpenApiSchema for DriveEntry {}
impl api_macros::openapi::OpenApiSchema for DriveEntryQuery {}
impl api_macros::openapi::OpenApiSchema for DriveListQuery {}
impl api_macros::openapi::OpenApiSchema for DriveSearchQuery {}
impl api_macros::openapi::OpenApiSchema for DriveListResponse {}
impl api_macros::openapi::OpenApiSchema for DriveReadTextQuery {}
impl api_macros::openapi::OpenApiSchema for DriveReadTextResponse {}
impl api_macros::openapi::OpenApiSchema for DriveReadChunkQuery {}
impl api_macros::openapi::OpenApiSchema for DriveDownloadQuery {}
impl api_macros::openapi::OpenApiSchema for DriveMutationRequest {}
impl api_macros::openapi::OpenApiSchema for DriveMutationResponse {}
impl api_macros::openapi::OpenApiSchema for DriveUploadQuery {}
impl api_macros::openapi::OpenApiSchema for DriveRequestStatusResponse {}
impl api_macros::openapi::OpenApiSchema for DriveGrantCreateRequest {}
impl api_macros::openapi::OpenApiSchema for DriveGrantResponse {}
impl api_macros::openapi::OpenApiSchema for DriveGrantListQuery {}
impl api_macros::openapi::OpenApiSchema for DriveGrantListResponse {}
/// Browser wire declarations are derived from the transport DTOs, not copied
/// from storage models or maintained independently in the Web client.
#[cfg(feature = "typescript")]
pub fn drive_api_typescript() -> String {
use ts_rs::TS;
let config = ts_rs::Config::default();
let declarations = [
DriveEntryRef::decl(&config),
DriveEntryKind::decl(&config),
DriveEntry::decl(&config),
DriveEntryQuery::decl(&config),
DriveListQuery::decl(&config),
DriveSearchQuery::decl(&config),
DriveListResponse::decl(&config),
DriveReadTextQuery::decl(&config),
DriveReadTextResponse::decl(&config),
DriveReadChunkQuery::decl(&config),
DriveDownloadQuery::decl(&config),
DriveMutation::decl(&config),
DriveMutationRequest::decl(&config),
DriveMutationResponse::decl(&config),
DriveUploadOperation::decl(&config),
DriveUploadQuery::decl(&config),
DriveRequestState::decl(&config),
DriveRequestStatusResponse::decl(&config),
DriveAccess::decl(&config),
DriveGrantCreateRequest::decl(&config),
DriveGrantResponse::decl(&config),
DriveGrantListQuery::decl(&config),
DriveGrantListResponse::decl(&config),
DriveApiErrorCode::decl(&config),
DriveFailureClassification::decl(&config),
DriveApiError::decl(&config),
];
format!(
"// Generated from server-api. Do not edit by hand.\n// Regenerate: cargo run -q -p server-api --features typescript --example generate_drive_api_types > web/workspace/src/lib/generated/drive-api.ts\n\n{}\n",
declarations
.into_iter()
.map(|declaration| format!("export {declaration}"))
.collect::<Vec<_>>()
.join("\n\n")
)
}
#[cfg(all(test, feature = "typescript"))]
mod typescript_tests {
#[test]
fn generated_drive_api_contract_is_current() {
let expected = super::drive_api_typescript();
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../web/workspace/src/lib/generated/drive-api.ts");
let actual = std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
assert_eq!(
normalize(&actual),
normalize(&expected),
"regenerate Drive API TypeScript types with `cargo run -q -p server-api --features typescript --example generate_drive_api_types > web/workspace/src/lib/generated/drive-api.ts` and format the generated file"
);
}
fn normalize(value: &str) -> String {
value
.chars()
.filter_map(|character| match character {
character if character.is_whitespace() => None,
',' => Some(';'),
character => Some(character),
})
.collect::<String>()
.replace("=|", "=")
.replace(";}", "}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ApiContract, HttpMethod, ServerApiMetadata, canonical_openapi_document};
use serde_json::json;
#[test]
fn drive_entry_references_reject_unbound_or_noncanonical_numeric_ids() {
let valid = json!({"workspace_id":"workspace-a", "node_id":"9007199254740993"});
let entry: DriveEntryRef = serde_json::from_value(valid.clone()).unwrap();
assert_eq!(serde_json::to_value(entry).unwrap(), valid);
for invalid in [
json!("7"),
json!(7),
json!({"node_id":"7"}),
json!({"workspace_id":"a", "node_id":7}),
] {
assert!(serde_json::from_value::<DriveEntryRef>(invalid).is_err());
}
for id in [
"",
"0",
"01",
"-1",
"+1",
"1.0",
"1e3",
" 1",
"١",
"9223372036854775808",
"18446744073709551616",
] {
assert!(
serde_json::from_value::<DriveEntryRef>(json!({"workspace_id":"a", "node_id":id}))
.is_err(),
"accepted {id:?}"
);
assert!(
serde_json::from_value::<DriveEntryQuery>(
json!({"entry_workspace_id":"a", "id":id})
)
.is_err()
);
}
assert!(
serde_json::from_value::<DriveEntryRef>(
json!({"workspace_id":"a","node_id":"9223372036854775807"})
)
.is_ok()
);
assert!(serde_json::from_value::<DriveEntryQuery>(json!({"id":"7"})).is_err());
assert!(serde_json::from_value::<DriveListQuery>(json!({"id":"7"})).is_err());
assert!(serde_json::from_value::<DriveReadTextQuery>(json!({"id":"7"})).is_err());
assert!(serde_json::from_value::<DriveDownloadQuery>(json!({"id":"7"})).is_err());
assert!(
serde_json::from_value::<DriveEntryQuery>(
json!({"entry_workspace_id":"a", "id":"7", "worker_id":"spoofed"})
)
.is_err()
);
}
#[test]
fn drive_metadata_exposes_only_stored_update_provenance_and_same_workspace_url() {
let value = json!({
"entry":{"workspace_id":"a","node_id":"7"},
"parent":{"workspace_id":"a","node_id":"1"},
"name":"file.txt","kind":"file","revision":"2","size":5,
"content_type":"text/plain","updated_by":"user-a","updated_at":"2026-10-08T00:00:00Z",
"latest_url":"/api/w/a/drive/download?entry_workspace_id=a&id=7"
});
let entry: DriveEntry = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(entry).unwrap(), value);
for field in ["sha256", "created_at"] {
let mut fabricated = value.clone();
fabricated[field] = json!("not stored");
assert!(serde_json::from_value::<DriveEntry>(fabricated).is_err());
}
}
#[test]
fn drive_search_text_is_opt_in_and_work_budget_is_bounded() {
let query: DriveSearchQuery = serde_json::from_value(json!({"query":"name"})).unwrap();
assert!(!query.include_text);
let query: DriveSearchQuery =
serde_json::from_value(json!({"query":"text","include_text":true,"limit":128}))
.unwrap();
assert!(query.include_text);
for limit in [0, 129, u32::MAX] {
assert!(
serde_json::from_value::<DriveSearchQuery>(json!({"query":"name","limit":limit}))
.is_err()
);
}
}
#[test]
fn drive_request_status_is_a_commit_snapshot_not_a_fictitious_lifecycle() {
for state in ["uncommitted", "committed"] {
let value = json!({"request_id":"r","state":state,"response":null});
let response: DriveRequestStatusResponse =
serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(response).unwrap(), value);
}
for state in ["pending", "unknown", "not_committed"] {
assert!(
serde_json::from_value::<DriveRequestStatusResponse>(
json!({"request_id":"r","state":state,"response":null})
)
.is_err()
);
}
}
#[test]
fn drive_json_mutations_accept_only_bounded_text_and_workspace_bound_cas() {
let valid = json!({"request_id":"request-a", "mutation":{
"operation":"update_text", "id":{"workspace_id":"a","node_id":"7"},
"expected_revision":"9007199254740993", "text":"hello", "content_type":"text/plain"
}});
let request: DriveMutationRequest = serde_json::from_value(valid.clone()).unwrap();
assert_eq!(serde_json::to_value(request).unwrap(), valid);
let mut invalid = valid.clone();
invalid["mutation"]["bytes"] = json!([0, 255]);
assert!(serde_json::from_value::<DriveMutationRequest>(invalid).is_err());
let mut invalid = valid.clone();
invalid["mutation"]["id"] = json!("7");
assert!(serde_json::from_value::<DriveMutationRequest>(invalid).is_err());
let mut invalid = valid.clone();
invalid["mutation"]["expected_revision"] = json!("01");
assert!(serde_json::from_value::<DriveMutationRequest>(invalid).is_err());
// UTF-8 byte count, not character count, determines the text bound.
let mut boundary = valid;
boundary["mutation"]["text"] = json!("é".repeat(DRIVE_TEXT_MAX_BYTES / 2));
assert!(serde_json::from_value::<DriveMutationRequest>(boundary.clone()).is_ok());
boundary["mutation"]["text"] = json!("é".repeat(DRIVE_TEXT_MAX_BYTES / 2 + 1));
assert!(serde_json::from_value::<DriveMutationRequest>(boundary).is_err());
}
#[test]
fn drive_reads_reject_zero_or_over_64kib_requests() {
for size in [0, 65537, u32::MAX] {
assert!(
serde_json::from_value::<DriveReadTextQuery>(
json!({"entry_workspace_id":"a","id":"7","max_bytes":size})
)
.is_err()
);
assert!(serde_json::from_value::<DriveReadChunkQuery>(json!({"entry_workspace_id":"a","id":"7","expected_revision":"1","offset":0,"length":size})).is_err());
}
let default: DriveReadTextQuery =
serde_json::from_value(json!({"entry_workspace_id":"a","id":"7"})).unwrap();
assert_eq!(default.max_bytes, 65536);
let chunk: DriveReadChunkQuery = serde_json::from_value(json!({"entry_workspace_id":"a","id":"7","expected_revision":"1","offset":0,"length":65536})).unwrap();
assert_eq!(chunk.length, 65536);
}
#[test]
fn drive_upload_requires_binding_declared_integrity_and_request_identity() {
let valid = json!({"operation":"update","request_id":"request-a","entry_workspace_id":"a","id":"7","expected_revision":"2","content_type":"application/octet-stream","size":3,"sha256":"a".repeat(64)});
assert!(serde_json::from_value::<DriveUploadQuery>(valid.clone()).is_ok());
for field in ["entry_workspace_id", "size", "sha256", "request_id"] {
let mut missing = valid.clone();
missing.as_object_mut().unwrap().remove(field);
assert!(
serde_json::from_value::<DriveUploadQuery>(missing).is_err(),
"accepted missing {field}"
);
}
for size in [DRIVE_FILE_MAX_BYTES, DRIVE_FILE_MAX_BYTES + 1, u32::MAX] {
let mut oversized = valid.clone();
oversized["size"] = json!(size);
let query: DriveUploadQuery = serde_json::from_value(oversized).unwrap();
assert_eq!(
query.size, size,
"service must receive oversized declared sizes for typed Limit classification"
);
}
assert!(serde_json::from_value::<DriveReadChunkQuery>(json!({"entry_workspace_id":"a","id":"7","expected_revision":"1","offset":DRIVE_FILE_MAX_BYTES,"length":1})).is_ok());
let mut invalid = valid;
invalid["bytes"] = json!([0, 255]);
assert!(serde_json::from_value::<DriveUploadQuery>(invalid).is_err());
}
#[test]
fn drive_grants_reject_command_access_and_unbounded_pages() {
let grant = json!({"runtime_id":"r","worker_id":"w","access":"read_write"});
assert_eq!(
serde_json::to_value(
serde_json::from_value::<DriveGrantCreateRequest>(grant.clone()).unwrap()
)
.unwrap(),
grant
);
assert!(
serde_json::from_value::<DriveGrantCreateRequest>(
json!({"runtime_id":"r","worker_id":"w","access":"read_write_command"})
)
.is_err()
);
assert!(serde_json::from_value::<DriveGrantCreateRequest>(json!({"runtime_id":"r","worker_id":"w","access":"read_only","workspace_id":"spoofed"})).is_err());
for limit in [0, 201, u32::MAX] {
assert!(serde_json::from_value::<DriveGrantListQuery>(json!({"limit":limit})).is_err());
}
assert!(
serde_json::from_value::<DriveGrantListQuery>(json!({"limit":200,"after":"7"})).is_ok()
);
assert!(serde_json::from_value::<DriveGrantListQuery>(json!({"after":"01"})).is_err());
}
#[test]
fn drive_errors_have_fixed_safe_messages_and_typed_outcome_classification() {
use DriveApiErrorCode::*;
for (code, wire, status, message) in [
(Denied, "denied", 403, "Drive access denied"),
(NotFound, "not_found", 404, "Drive resource not found"),
(Conflict, "conflict", 409, "Drive revision or name conflict"),
(Invalid, "invalid", 400, "Invalid Drive request"),
(Limit, "limit", 413, "Drive request exceeds a limit"),
(
StorageUnavailable,
"storage_unavailable",
503,
"Drive storage unavailable",
),
(
OutcomeUnknown,
"outcome_unknown",
503,
"Drive mutation outcome unknown",
),
] {
let error = DriveApiError::new(code);
assert_eq!(api_macros::HttpError::status_code(&error), status);
assert_eq!(
serde_json::to_value(&error).unwrap(),
json!({"code":wire,"classification":if code == OutcomeUnknown {"unknown"} else {"not_committed"},"message":message})
);
}
for status in [400, 413, 415, 422] {
let error = <DriveApiError as api_macros::HttpRequestError>::from_request_rejection(
status,
"/private/path: SQL failed".into(),
);
assert_eq!(
error,
DriveApiError::new(if status == 413 { Limit } else { Invalid })
);
}
assert!(serde_json::from_value::<DriveApiError>(json!({"code":"invalid","classification":"not_committed","message":"Invalid Drive request","provider_error":"secret"})).is_err());
}
#[test]
fn drive_routes_are_workspace_scoped_authenticated_and_binary_is_not_json() {
let document = canonical_openapi_document().unwrap();
let document: serde_json::Value =
serde_json::from_str(&document.to_json().unwrap()).unwrap();
let schemas = &document["components"]["schemas"];
assert_eq!(
schemas["DriveEntry"]["properties"]["size"]["maximum"],
DRIVE_FILE_MAX_BYTES
);
assert_eq!(
schemas["DriveReadChunkQuery"]["properties"]["offset"]["maximum"],
DRIVE_FILE_MAX_BYTES
);
assert_eq!(
schemas["DriveUploadQuery"]["properties"]["size"]["maximum"],
DRIVE_FILE_MAX_BYTES
);
assert_eq!(
schemas["DriveSearchQuery"]["properties"]["limit"]["maximum"],
DRIVE_SEARCH_MAX_LIMIT
);
for (name, method, suffix, wire_method) in [
("drive_root", HttpMethod::Get, "/root", "get"),
("drive_metadata", HttpMethod::Get, "/metadata", "get"),
("drive_list", HttpMethod::Get, "/list", "get"),
("drive_search", HttpMethod::Get, "/search", "get"),
("drive_read_text", HttpMethod::Get, "/read-text", "get"),
("drive_read_chunk", HttpMethod::Get, "/read-chunk", "get"),
("drive_download", HttpMethod::Get, "/download", "get"),
("drive_mutate", HttpMethod::Post, "/mutate", "post"),
("drive_upload", HttpMethod::Put, "/upload", "put"),
(
"drive_request_status",
HttpMethod::Get,
"/requests/{request_id}",
"get",
),
("drive_grant_create", HttpMethod::Post, "/grants", "post"),
(
"drive_grant_revoke",
HttpMethod::Delete,
"/grants/{grant_id}",
"delete",
),
("drive_grant_list", HttpMethod::Get, "/grants", "get"),
] {
let operation = ServerApiMetadata::OPERATIONS
.iter()
.find(|operation| operation.operation_id == name)
.unwrap();
let path = format!("/api/w/{{workspace_id}}/drive{suffix}");
assert_eq!(operation.method, method);
assert_eq!(operation.path, path);
let documented = &document["paths"][&path][wire_method];
assert_eq!(documented["operationId"], name);
assert_eq!(
documented["security"],
json!([{"bearerAuth":[]},{"browserSession":[]}])
);
// Server extensions must not become caller-controlled parameters.
assert!(
documented["parameters"]
.as_array()
.unwrap()
.iter()
.all(|parameter| parameter["name"] != "context")
);
if matches!(name, "drive_read_chunk" | "drive_download") {
let content = &documented["responses"]["200"]["content"];
assert!(content["application/octet-stream"].is_object());
assert!(content["application/json"].is_null());
}
if name == "drive_download" {
let headers = documented["responses"]["200"]["headers"]
.as_object()
.unwrap();
assert_eq!(headers.len(), 5);
for header in [
"content-disposition",
"cache-control",
"etag",
"x-content-type-options",
"content-security-policy",
] {
assert_eq!(
headers[header]["schema"]["$ref"],
"#/components/schemas/string"
);
}
assert!(!headers.contains_key("content-type"));
assert!(!headers.contains_key("content-length"));
}
if name == "drive_upload" {
let content = &documented["requestBody"]["content"];
assert!(content["application/octet-stream"].is_object());
assert!(content["application/json"].is_null());
}
}
}
}
+112
View File
@@ -10,7 +10,9 @@ use api_macros::api;
pub use api_macros::axum as server_support;
pub use api_macros::reqwest as client_support;
pub use api_macros::{ApiContract, BinaryBody, HttpMethod, TransportMetadata, WebSocketOperation};
mod drive;
pub mod repository_openapi_typescript;
pub use drive::*;
mod workspace_config;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -2603,6 +2605,116 @@ pub trait ServerApi {
#[path] working_directory_id: String,
#[body] request: WorkingDirectoryRemovalRequest,
) -> Result<WorkingDirectoryRemovalResponse, RepositoryApiError>;
// Ordinary operations accept member browser sessions or trusted Worker request
// context. The service resolves Worker grants; no operation accepts an actor or
// grant ID from the caller. Grant management is Workspace-owner-only.
#[get("/api/w/{workspace_id}/drive/root", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_root(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
) -> Result<DriveEntry, DriveApiError>;
#[get("/api/w/{workspace_id}/drive/metadata", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_metadata(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[query] query: DriveEntryQuery,
) -> Result<DriveEntry, DriveApiError>;
#[get("/api/w/{workspace_id}/drive/list", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_list(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[query] query: DriveListQuery,
) -> Result<DriveListResponse, DriveApiError>;
#[get("/api/w/{workspace_id}/drive/search", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_search(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[query] query: DriveSearchQuery,
) -> Result<DriveListResponse, DriveApiError>;
#[get("/api/w/{workspace_id}/drive/read-text", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_read_text(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[query] query: DriveReadTextQuery,
) -> Result<DriveReadTextResponse, DriveApiError>;
#[get("/api/w/{workspace_id}/drive/read-chunk", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_read_chunk(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[query] query: DriveReadChunkQuery,
) -> Result<BinaryBody, DriveApiError>;
// Download safety/cache headers are service-produced typed response metadata;
// Content-Type/Content-Length remain controlled by the binary transport.
#[get(
"/api/w/{workspace_id}/drive/download",
responses = [(status = 200, body = BinaryBody, headers = [
("content-disposition", String),
("cache-control", String),
("etag", String),
("x-content-type-options", String),
("content-security-policy", String)
])],
error_status = 400,
additional_error_statuses = [401,403,404,409,413,503],
bearer_auth = true,
browser_auth = true
)]
async fn drive_download(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[query] query: DriveDownloadQuery,
) -> Result<server_api_responses::DriveDownload, DriveApiError>;
#[post("/api/w/{workspace_id}/drive/mutate", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true, normalize_body_errors = true)]
async fn drive_mutate(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[body] request: DriveMutationRequest,
) -> Result<DriveMutationResponse, DriveApiError>;
#[put("/api/w/{workspace_id}/drive/upload", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true, normalize_body_errors = true)]
async fn drive_upload(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[query] query: DriveUploadQuery,
#[binary] body: BinaryBody,
) -> Result<DriveMutationResponse, DriveApiError>;
#[get("/api/w/{workspace_id}/drive/requests/{request_id}", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_request_status(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[path] request_id: String,
) -> Result<DriveRequestStatusResponse, DriveApiError>;
#[post("/api/w/{workspace_id}/drive/grants", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true, normalize_body_errors = true)]
async fn drive_grant_create(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[body] request: DriveGrantCreateRequest,
) -> Result<DriveGrantResponse, DriveApiError>;
#[delete("/api/w/{workspace_id}/drive/grants/{grant_id}", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_grant_revoke(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[path] grant_id: String,
) -> Result<DriveGrantResponse, DriveApiError>;
#[get("/api/w/{workspace_id}/drive/grants", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,503], bearer_auth = true, browser_auth = true)]
async fn drive_grant_list(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[query] query: DriveGrantListQuery,
) -> Result<DriveGrantListResponse, DriveApiError>;
#[get("/api/w/{workspace_id}/workers/self/workspace-config", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,500,503], openapi = false)]
async fn current_worker_workspace_config_get(
&self,