From ecf47fbcc2f00b1d10b61f8f6805c266f507e168 Mon Sep 17 00:00:00 2001 From: "Michael K (Pear)" Date: Mon, 31 Aug 2026 04:10:25 -0400 Subject: [PATCH 1/5] feat(spacetimedb): add the SpacetimeDB SDK --- .gitmodules | 5 + .../.generated-specs/spacetimedb.json | 1188 +++++++++++++++++ packages/spacetimedb/package.json | 85 ++ packages/spacetimedb/scripts/convert.ts | 29 + packages/spacetimedb/scripts/generate.ts | 112 ++ packages/spacetimedb/src/credentials.ts | 73 + packages/spacetimedb/src/errors.ts | 50 + packages/spacetimedb/src/index.ts | 30 + packages/spacetimedb/src/pagination.ts | 7 + packages/spacetimedb/src/protocol.ts | 67 + packages/spacetimedb/src/retry.ts | 56 + packages/spacetimedb/src/services/index.ts | 2 + .../spacetimedb/src/services/spacetimedb.ts | 891 +++++++++++++ packages/spacetimedb/src/traits.ts | 44 + packages/spacetimedb/tsconfig.json | 14 + packages/spacetimedb/tsconfig.scripts.json | 15 + stacks/distilled-submodules/SpecRepos.ts | 1 + .../spec-repos/spacetimedb/fetch-specs.ts | 908 +++++++++++++ .../spec-repos/spacetimedb/package.json | 27 + .../spec-repos/spacetimedb/readme.md | 31 + tsconfig.json | 6 + 21 files changed, 3641 insertions(+) create mode 100644 packages/spacetimedb/.generated-specs/spacetimedb.json create mode 100644 packages/spacetimedb/package.json create mode 100644 packages/spacetimedb/scripts/convert.ts create mode 100644 packages/spacetimedb/scripts/generate.ts create mode 100644 packages/spacetimedb/src/credentials.ts create mode 100644 packages/spacetimedb/src/errors.ts create mode 100644 packages/spacetimedb/src/index.ts create mode 100644 packages/spacetimedb/src/pagination.ts create mode 100644 packages/spacetimedb/src/protocol.ts create mode 100644 packages/spacetimedb/src/retry.ts create mode 100644 packages/spacetimedb/src/services/index.ts create mode 100644 packages/spacetimedb/src/services/spacetimedb.ts create mode 100644 packages/spacetimedb/src/traits.ts create mode 100644 packages/spacetimedb/tsconfig.json create mode 100644 packages/spacetimedb/tsconfig.scripts.json create mode 100644 stacks/distilled-submodules/spec-repos/spacetimedb/fetch-specs.ts create mode 100644 stacks/distilled-submodules/spec-repos/spacetimedb/package.json create mode 100644 stacks/distilled-submodules/spec-repos/spacetimedb/readme.md diff --git a/.gitmodules b/.gitmodules index 8bed9f12fc..3df6779a82 100644 --- a/.gitmodules +++ b/.gitmodules @@ -353,3 +353,8 @@ url = https://github.com/distilled-mirror/spec-mirror-modrinth.git ignore = dirty shallow = true +[submodule "packages/spacetimedb/specs/spec-mirror-spacetimedb"] + path = packages/spacetimedb/specs/spec-mirror-spacetimedb + url = https://github.com/distilled-mirror/spec-mirror-spacetimedb.git + ignore = dirty + shallow = true diff --git a/packages/spacetimedb/.generated-specs/spacetimedb.json b/packages/spacetimedb/.generated-specs/spacetimedb.json new file mode 100644 index 0000000000..47d955b5f8 --- /dev/null +++ b/packages/spacetimedb/.generated-specs/spacetimedb.json @@ -0,0 +1,1188 @@ +{ + "smithy": "2.0", + "metadata": { + "suppressions": [ + { + "id": "HttpUriConflict", + "namespace": "*" + }, + { + "id": "HttpMethodSemantics", + "namespace": "*" + }, + { + "id": "UnreferencedShape", + "namespace": "*" + } + ] + }, + "shapes": { + "com.spacetimedb.api#Ping": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "smithy.api#Unit" + }, + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/ping", + "code": 200 + }, + "smithy.api#documentation": "No-op connectivity probe.\n\nDoes nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB." + } + }, + "com.spacetimedb.api#PublishDatabaseRequest": { + "type": "structure", + "members": { + "body": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#PublishDatabaseResponseSuccessOp": { + "type": "enum", + "members": { + "CREATED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "created" + } + }, + "UPDATED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "updated" + } + } + }, + "traits": {} + }, + "com.spacetimedb.api#PublishDatabaseResponseSuccess": { + "type": "structure", + "members": { + "database_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#required": {} + } + }, + "op": { + "target": "com.spacetimedb.api#PublishDatabaseResponseSuccessOp", + "traits": { + "smithy.api#required": {} + } + } + }, + "traits": {} + }, + "com.spacetimedb.api#PublishDatabaseResponse": { + "type": "structure", + "members": { + "Success": { + "target": "com.spacetimedb.api#PublishDatabaseResponseSuccess", + "traits": { + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#PublishDatabase": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#PublishDatabaseRequest" + }, + "output": { + "target": "com.spacetimedb.api#PublishDatabaseResponse" + }, + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/v1/database", + "code": 200 + }, + "smithy.api#documentation": "Publish a new database given its module code.\n\nPublish a new database with no name. Accessible through the CLI as `spacetime publish`. Request body is a WebAssembly module in the binary format. If no Authorization header is provided, a new anonymous identity owns the database." + } + }, + "com.spacetimedb.api#GetDatabaseRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#GetDatabaseResponseHostType": { + "type": "enum", + "members": { + "WASM": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "wasm" + } + } + }, + "traits": { + "smithy.api#documentation": "The module host type; currently always \"wasm\"." + } + }, + "com.spacetimedb.api#GetDatabaseResponse": { + "type": "structure", + "members": { + "database_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The Spacetime identity of the database.", + "smithy.api#required": {} + } + }, + "owner_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "The Spacetime identity of the database's owner.", + "smithy.api#required": {} + } + }, + "host_type": { + "target": "com.spacetimedb.api#GetDatabaseResponseHostType", + "traits": { + "smithy.api#documentation": "The module host type; currently always \"wasm\".", + "smithy.api#required": {} + } + }, + "initial_program": { + "target": "smithy.api#String", + "traits": { + "smithy.api#documentation": "Hash of the WASM module with which the database was initialized.", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#GetDatabase": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#GetDatabaseRequest" + }, + "output": { + "target": "com.spacetimedb.api#GetDatabaseResponse" + }, + "errors": [ + { + "target": "com.spacetimedb.api#NotFound" + } + ], + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/database/{name_or_identity}", + "code": 200 + }, + "smithy.api#documentation": "Get a JSON description of a database.\n\nGet a database's identity, owner identity, host type, and a hash of its WASM module." + } + }, + "com.spacetimedb.api#UpdateDatabaseRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + }, + "clear": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#httpQuery": "clear", + "smithy.api#documentation": "Whether to clear any existing data when updating an existing database." + } + }, + "body": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#UpdateDatabaseResponseSuccessOp": { + "type": "enum", + "members": { + "CREATED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "created" + } + }, + "UPDATED": { + "target": "smithy.api#Unit", + "traits": { + "smithy.api#enumValue": "updated" + } + } + }, + "traits": {} + }, + "com.spacetimedb.api#UpdateDatabaseResponseSuccess": { + "type": "structure", + "members": { + "domain": { + "target": "smithy.api#String", + "traits": { + "com.distilled.openapi#nullable": {} + } + }, + "database_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#required": {} + } + }, + "op": { + "target": "com.spacetimedb.api#UpdateDatabaseResponseSuccessOp", + "traits": { + "smithy.api#required": {} + } + } + }, + "traits": {} + }, + "com.spacetimedb.api#UpdateDatabaseResponse": { + "type": "structure", + "members": { + "Success": { + "target": "com.spacetimedb.api#UpdateDatabaseResponseSuccess", + "traits": { + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#UpdateDatabase": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#UpdateDatabaseRequest" + }, + "output": { + "target": "com.spacetimedb.api#UpdateDatabaseResponse" + }, + "traits": { + "smithy.api#http": { + "method": "PUT", + "uri": "/v1/database/{name_or_identity}", + "code": 200 + }, + "smithy.api#documentation": "Publish to a database given its module code.\n\nPublish to a database with the specified name or identity. If the name does not exist, creates a new database. Accessible through the CLI as `spacetime publish`." + } + }, + "com.spacetimedb.api#DeleteDatabaseRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#DeleteDatabase": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#DeleteDatabaseRequest" + }, + "output": { + "target": "smithy.api#Unit" + }, + "errors": [ + { + "target": "com.spacetimedb.api#NotFound" + } + ], + "traits": { + "smithy.api#http": { + "method": "DELETE", + "uri": "/v1/database/{name_or_identity}", + "code": 200 + }, + "smithy.api#documentation": "Delete a database.\n\nDelete a database. Accessible through the CLI as `spacetime delete`. Deleting a database requires ownership." + } + }, + "com.spacetimedb.api#GetDatabaseNamesRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#GetDatabaseNamesResponseNamesList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": {} + }, + "com.spacetimedb.api#GetDatabaseNamesResponse": { + "type": "structure", + "members": { + "names": { + "target": "com.spacetimedb.api#GetDatabaseNamesResponseNamesList", + "traits": { + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#GetDatabaseNames": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#GetDatabaseNamesRequest" + }, + "output": { + "target": "com.spacetimedb.api#GetDatabaseNamesResponse" + }, + "errors": [ + { + "target": "com.spacetimedb.api#NotFound" + } + ], + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/database/{name_or_identity}/names", + "code": 200 + }, + "smithy.api#documentation": "Get the names this database can be identified by." + } + }, + "com.spacetimedb.api#AddDatabaseNameRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + }, + "body": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#AddDatabaseNameResponseSuccess": { + "type": "structure", + "members": { + "domain": { + "target": "smithy.api#String" + }, + "database_result": { + "target": "smithy.api#String" + } + }, + "traits": {} + }, + "com.spacetimedb.api#AddDatabaseNameResponse": { + "type": "structure", + "members": { + "Success": { + "target": "com.spacetimedb.api#AddDatabaseNameResponseSuccess", + "traits": { + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#AddDatabaseName": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#AddDatabaseNameRequest" + }, + "output": { + "target": "com.spacetimedb.api#AddDatabaseNameResponse" + }, + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/v1/database/{name_or_identity}/names", + "code": 200 + }, + "smithy.api#documentation": "Add a new name for this database.\n\nTakes as the request body a string containing the new name of the database." + } + }, + "com.spacetimedb.api#SetDatabaseNamesRequestBodyList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#documentation": "Names for this database." + } + }, + "com.spacetimedb.api#SetDatabaseNamesRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + }, + "body": { + "target": "com.spacetimedb.api#SetDatabaseNamesRequestBodyList", + "traits": { + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#SetDatabaseNamesResponse": { + "type": "structure", + "members": { + "Success": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#required": {}, + "com.distilled.openapi#nullable": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#SetDatabaseNames": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#SetDatabaseNamesRequest" + }, + "output": { + "target": "com.spacetimedb.api#SetDatabaseNamesResponse" + }, + "traits": { + "smithy.api#http": { + "method": "PUT", + "uri": "/v1/database/{name_or_identity}/names", + "code": 200 + }, + "smithy.api#documentation": "Set the list of names for this database.\n\nSetting names requires ownership of the database." + } + }, + "com.spacetimedb.api#GetDatabaseIdentityRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#GetDatabaseIdentityResponse": { + "type": "structure", + "members": { + "body": { + "target": "smithy.api#String", + "traits": { + "com.distilled.openapi#rawResponse": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#GetDatabaseIdentity": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#GetDatabaseIdentityRequest" + }, + "output": { + "target": "com.spacetimedb.api#GetDatabaseIdentityResponse" + }, + "errors": [ + { + "target": "com.spacetimedb.api#NotFound" + } + ], + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/database/{name_or_identity}/identity", + "code": 200 + }, + "smithy.api#documentation": "Get the identity of a database.\n\nReturns a hex string of the specified database's identity." + } + }, + "com.spacetimedb.api#CallReducerRequestBodyList": { + "type": "list", + "member": { + "target": "smithy.api#Document" + }, + "traits": { + "smithy.api#documentation": "JSON array of arguments to the reducer." + } + }, + "com.spacetimedb.api#CallReducerRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + }, + "reducer": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "The name of the reducer or procedure." + } + }, + "body": { + "target": "com.spacetimedb.api#CallReducerRequestBodyList", + "traits": { + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#CallReducerResponse": { + "type": "structure", + "members": { + "body": { + "target": "smithy.api#Document", + "traits": { + "com.distilled.openapi#rawResponse": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#CallReducer": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#CallReducerRequest" + }, + "output": { + "target": "com.spacetimedb.api#CallReducerResponse" + }, + "errors": [ + { + "target": "com.spacetimedb.api#NotFound" + } + ], + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/v1/database/{name_or_identity}/call/{reducer}", + "code": 200 + }, + "smithy.api#documentation": "Invoke a reducer or procedure in a database.\n\nThe caller's identity is passed to the reducer via its ReducerContext. Request body is a JSON array of arguments." + } + }, + "com.spacetimedb.api#GetDatabaseSchemaRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + }, + "version": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#httpQuery": "version", + "smithy.api#documentation": "The version of RawModuleDef to return, e.g. 9." + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#GetDatabaseSchemaResponseBodyMap": { + "type": "map", + "key": { + "target": "smithy.api#String" + }, + "value": { + "target": "smithy.api#Document" + }, + "traits": {} + }, + "com.spacetimedb.api#GetDatabaseSchemaResponse": { + "type": "structure", + "members": { + "body": { + "target": "com.spacetimedb.api#GetDatabaseSchemaResponseBodyMap", + "traits": { + "com.distilled.openapi#rawResponse": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#GetDatabaseSchema": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#GetDatabaseSchemaRequest" + }, + "output": { + "target": "com.spacetimedb.api#GetDatabaseSchemaResponse" + }, + "errors": [ + { + "target": "com.spacetimedb.api#NotFound" + } + ], + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/database/{name_or_identity}/schema", + "code": 200 + }, + "smithy.api#documentation": "Get the schema for a database.\n\nReturns a RawModuleDef in JSON form. Accessible through the CLI as `spacetime describe`." + } + }, + "com.spacetimedb.api#GetDatabaseLogsRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + }, + "num_lines": { + "target": "smithy.api#Integer", + "traits": { + "smithy.api#httpQuery": "num_lines", + "smithy.api#documentation": "Number of most-recent log lines to retrieve." + } + }, + "follow": { + "target": "smithy.api#Boolean", + "traits": { + "smithy.api#httpQuery": "follow", + "smithy.api#documentation": "Whether to continue receiving new logs via a stream." + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#GetDatabaseLogsResponse": { + "type": "structure", + "members": { + "body": { + "target": "smithy.api#String", + "traits": { + "com.distilled.openapi#rawResponse": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#GetDatabaseLogs": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#GetDatabaseLogsRequest" + }, + "output": { + "target": "com.spacetimedb.api#GetDatabaseLogsResponse" + }, + "errors": [ + { + "target": "com.spacetimedb.api#NotFound" + } + ], + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/database/{name_or_identity}/logs", + "code": 200 + }, + "smithy.api#documentation": "Retrieve logs from a database.\n\nViewing logs requires ownership of the database. Accessible through the CLI as `spacetime logs`. Returns text, or streaming text if follow is supplied." + } + }, + "com.spacetimedb.api#ExecuteSqlRequest": { + "type": "structure", + "members": { + "name_or_identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "Database name or Spacetime identity." + } + }, + "body": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpPayload": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#ExecuteSqlResponseBodyItemRowsList": { + "type": "list", + "member": { + "target": "smithy.api#Document" + }, + "traits": { + "smithy.api#documentation": "JSON-encoded ProductValues conforming to schema." + } + }, + "com.spacetimedb.api#ExecuteSqlResponseBodyItem": { + "type": "structure", + "members": { + "schema": { + "target": "smithy.api#Document", + "traits": { + "smithy.api#documentation": "JSON-encoded ProductType of the returned rows.", + "smithy.api#required": {} + } + }, + "rows": { + "target": "com.spacetimedb.api#ExecuteSqlResponseBodyItemRowsList", + "traits": { + "smithy.api#documentation": "JSON-encoded ProductValues conforming to schema.", + "smithy.api#required": {} + } + } + }, + "traits": {} + }, + "com.spacetimedb.api#ExecuteSqlResponseBodyList": { + "type": "list", + "member": { + "target": "com.spacetimedb.api#ExecuteSqlResponseBodyItem" + }, + "traits": {} + }, + "com.spacetimedb.api#ExecuteSqlResponse": { + "type": "structure", + "members": { + "body": { + "target": "com.spacetimedb.api#ExecuteSqlResponseBodyList", + "traits": { + "com.distilled.openapi#rawResponse": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#ExecuteSql": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#ExecuteSqlRequest" + }, + "output": { + "target": "com.spacetimedb.api#ExecuteSqlResponse" + }, + "errors": [ + { + "target": "com.spacetimedb.api#NotFound" + } + ], + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/v1/database/{name_or_identity}/sql", + "code": 200 + }, + "smithy.api#documentation": "Run a SQL query against a database.\n\nAccessible through the CLI as `spacetime sql`. Request body is SQL queries, separated by `;`. Anonymous callers only have access to public tables." + } + }, + "com.spacetimedb.api#CreateIdentityResponse": { + "type": "structure", + "members": { + "identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#required": {} + } + }, + "token": { + "target": "smithy.api#String", + "traits": { + "smithy.api#required": {}, + "smithy.api#sensitive": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#CreateIdentity": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "com.spacetimedb.api#CreateIdentityResponse" + }, + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/v1/identity", + "code": 200 + }, + "smithy.api#documentation": "Generate a new identity and token.\n\nCreate a new identity. The returned token is an OpenID Connect compliant JWT signed by this SpacetimeDB host and is not portable to other clusters." + } + }, + "com.spacetimedb.api#CreateWebsocketTokenResponse": { + "type": "structure", + "members": { + "token": { + "target": "smithy.api#String", + "traits": { + "smithy.api#required": {}, + "smithy.api#sensitive": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#CreateWebsocketToken": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "com.spacetimedb.api#CreateWebsocketTokenResponse" + }, + "traits": { + "smithy.api#http": { + "method": "POST", + "uri": "/v1/identity/websocket-token", + "code": 200 + }, + "smithy.api#documentation": "Generate a short-lived access token for use in untrusted contexts.\n\nGenerate a short-lived access token which can be used in untrusted contexts, e.g. embedded in URLs. Requires Authorization." + } + }, + "com.spacetimedb.api#GetPublicKeyResponse": { + "type": "structure", + "members": { + "body": { + "target": "smithy.api#String", + "traits": { + "com.distilled.openapi#rawResponse": {}, + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#GetPublicKey": { + "type": "operation", + "input": { + "target": "smithy.api#Unit" + }, + "output": { + "target": "com.spacetimedb.api#GetPublicKeyResponse" + }, + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/identity/public-key", + "code": 200 + }, + "smithy.api#documentation": "Get the public key used for verifying tokens.\n\nFetches the public key used by the database to verify tokens. Content-Type is application/pem-certificate-chain." + } + }, + "com.spacetimedb.api#ListIdentityDatabasesRequest": { + "type": "structure", + "members": { + "identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "A Spacetime identity." + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#ListIdentityDatabasesResponseIdentitiesList": { + "type": "list", + "member": { + "target": "smithy.api#String" + }, + "traits": { + "smithy.api#documentation": "Identities of databases owned by the path identity." + } + }, + "com.spacetimedb.api#ListIdentityDatabasesResponse": { + "type": "structure", + "members": { + "identities": { + "target": "com.spacetimedb.api#ListIdentityDatabasesResponseIdentitiesList", + "traits": { + "smithy.api#documentation": "Identities of databases owned by the path identity.", + "smithy.api#required": {} + } + } + }, + "traits": { + "smithy.api#output": {} + } + }, + "com.spacetimedb.api#ListIdentityDatabases": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#ListIdentityDatabasesRequest" + }, + "output": { + "target": "com.spacetimedb.api#ListIdentityDatabasesResponse" + }, + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/identity/{identity}/databases", + "code": 200 + }, + "smithy.api#documentation": "List databases owned by an identity." + } + }, + "com.spacetimedb.api#VerifyIdentityRequest": { + "type": "structure", + "members": { + "identity": { + "target": "smithy.api#String", + "traits": { + "smithy.api#httpLabel": {}, + "smithy.api#required": {}, + "smithy.api#documentation": "A Spacetime identity." + } + } + }, + "traits": { + "smithy.api#input": {} + } + }, + "com.spacetimedb.api#VerifyIdentity": { + "type": "operation", + "input": { + "target": "com.spacetimedb.api#VerifyIdentityRequest" + }, + "output": { + "target": "smithy.api#Unit" + }, + "errors": [ + { + "target": "com.spacetimedb.api#BadRequest" + } + ], + "traits": { + "smithy.api#http": { + "method": "GET", + "uri": "/v1/identity/{identity}/verify", + "code": 200 + }, + "smithy.api#documentation": "Verify an identity and token.\n\nVerify the validity of an identity/token pair. 204 if the token is valid and matches the identity; 400 if the token is valid but does not match; 401 if the token is invalid or missing." + } + }, + "com.spacetimedb.api#NotFound": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#error": "client", + "smithy.api#httpError": 404, + "com.distilled.openapi#errorMatchers": [ + { + "status": 404 + } + ] + } + }, + "com.spacetimedb.api#BadRequest": { + "type": "structure", + "members": {}, + "traits": { + "smithy.api#error": "client", + "smithy.api#httpError": 400, + "com.distilled.openapi#errorMatchers": [ + { + "status": 400 + } + ] + } + }, + "com.spacetimedb.api#SpacetimeDB": { + "type": "service", + "version": "2.0.0", + "operations": [ + { + "target": "com.spacetimedb.api#Ping" + }, + { + "target": "com.spacetimedb.api#PublishDatabase" + }, + { + "target": "com.spacetimedb.api#GetDatabase" + }, + { + "target": "com.spacetimedb.api#UpdateDatabase" + }, + { + "target": "com.spacetimedb.api#DeleteDatabase" + }, + { + "target": "com.spacetimedb.api#GetDatabaseNames" + }, + { + "target": "com.spacetimedb.api#AddDatabaseName" + }, + { + "target": "com.spacetimedb.api#SetDatabaseNames" + }, + { + "target": "com.spacetimedb.api#GetDatabaseIdentity" + }, + { + "target": "com.spacetimedb.api#CallReducer" + }, + { + "target": "com.spacetimedb.api#GetDatabaseSchema" + }, + { + "target": "com.spacetimedb.api#GetDatabaseLogs" + }, + { + "target": "com.spacetimedb.api#ExecuteSql" + }, + { + "target": "com.spacetimedb.api#CreateIdentity" + }, + { + "target": "com.spacetimedb.api#CreateWebsocketToken" + }, + { + "target": "com.spacetimedb.api#GetPublicKey" + }, + { + "target": "com.spacetimedb.api#ListIdentityDatabases" + }, + { + "target": "com.spacetimedb.api#VerifyIdentity" + } + ], + "traits": { + "smithy.api#title": "SpacetimeDB HTTP API", + "smithy.api#documentation": "HTTP management API for SpacetimeDB (/v1/database, /v1/identity, GET /v1/ping), assembled from https://spacetimedb.com/docs/http and https://spacetimedb.com/llms.txt. SpacetimeDB does not publish a first-party OpenAPI document. WebSocket subscribe and database-defined HTTP handler catch-alls are omitted." + } + } + } +} diff --git a/packages/spacetimedb/package.json b/packages/spacetimedb/package.json new file mode 100644 index 0000000000..a00407311f --- /dev/null +++ b/packages/spacetimedb/package.json @@ -0,0 +1,85 @@ +{ + "name": "@distilled.cloud/spacetimedb", + "version": "1.0.0-rc.8", + "repository": { + "type": "git", + "url": "https://github.com/alchemy-run/distilled", + "directory": "packages/spacetimedb" + }, + "license": "Apache-2.0", + "type": "module", + "sideEffects": false, + "files": [ + "lib", + "src" + ], + "exports": { + ".": { + "types": "./lib/index.d.ts", + "bun": "./src/index.ts", + "worker": "./src/index.ts", + "default": "./lib/index.js" + }, + "./Credentials": { + "types": "./lib/credentials.d.ts", + "bun": "./src/credentials.ts", + "worker": "./src/credentials.ts", + "default": "./lib/credentials.js" + }, + "./Errors": { + "types": "./lib/errors.d.ts", + "bun": "./src/errors.ts", + "worker": "./src/errors.ts", + "default": "./lib/errors.js" + }, + "./Traits": { + "types": "./lib/traits.d.ts", + "bun": "./src/traits.ts", + "worker": "./src/traits.ts", + "default": "./lib/traits.js" + }, + "./Pagination": { + "types": "./lib/pagination.d.ts", + "bun": "./src/pagination.ts", + "worker": "./src/pagination.ts", + "default": "./lib/pagination.js" + }, + "./Protocol": { + "types": "./lib/protocol.d.ts", + "bun": "./src/protocol.ts", + "worker": "./src/protocol.ts", + "default": "./lib/protocol.js" + }, + "./Retry": { + "types": "./lib/retry.d.ts", + "bun": "./src/retry.ts", + "worker": "./src/retry.ts", + "default": "./lib/retry.js" + }, + "./*": { + "types": "./lib/services/*.d.ts", + "bun": "./src/services/*.ts", + "worker": "./src/services/*.ts", + "default": "./lib/services/*.js" + } + }, + "scripts": { + "build": "tsc -b", + "convert": "bun scripts/convert.ts", + "generate": "bun scripts/convert.ts && bun scripts/generate.ts", + "typecheck": "tsc -p tsconfig.json", + "specs:fetch": "git submodule update --force --init --depth=1 specs/spec-mirror-spacetimedb", + "specs:update": "git submodule update --remote --force --depth=1 specs/spec-mirror-spacetimedb" + }, + "dependencies": { + "@distilled.cloud/core": "workspace:*" + }, + "devDependencies": { + "@types/bun": "catalog:tooling", + "@types/node": "catalog:tooling", + "@effect/platform-bun": "catalog:effect" + }, + "peerDependencies": { + "effect": "catalog:effect" + } +} diff --git a/packages/spacetimedb/scripts/convert.ts b/packages/spacetimedb/scripts/convert.ts new file mode 100644 index 0000000000..7cf1cccd36 --- /dev/null +++ b/packages/spacetimedb/scripts/convert.ts @@ -0,0 +1,29 @@ +#!/usr/bin/env bun +/** + * convert — turn the SpacetimeDB OpenAPI spec into a Smithy 2.0 JSON model. + * + * Input: specs/spec-mirror-spacetimedb/specs/openapi.json (spec submodule) + * Output: .generated-specs/spacetimedb.json + * + * The OpenAPI→Smithy converter lives in + * `@distilled.cloud/core/codegen/openapi`; this script is SpacetimeDB's + * pipeline config. `scripts/generate.ts` compiles the model into src/services. + */ +import * as path from "node:path"; +import { runOpenApiConvert } from "@distilled.cloud/core/codegen/openapi-cli"; + +await runOpenApiConvert({ + root: path.resolve(import.meta.dir, ".."), + specs: [ + { + name: "spacetimedb", + specPath: "specs/spec-mirror-spacetimedb/specs/openapi.json", + }, + ], + patchesDir: false, + options: { + namespace: "com.spacetimedb.api", + serviceName: "SpacetimeDB", + skipDeprecated: true, + }, +}); diff --git a/packages/spacetimedb/scripts/generate.ts b/packages/spacetimedb/scripts/generate.ts new file mode 100644 index 0000000000..7a3b480079 --- /dev/null +++ b/packages/spacetimedb/scripts/generate.ts @@ -0,0 +1,112 @@ +#!/usr/bin/env bun +/** + * generate — turn the Smithy JSON model in .generated-specs into the + * SpacetimeDB Effect SDK. + * + * Input: .generated-specs/spacetimedb.json (written by scripts/convert.ts) + * Output: src/services/spacetimedb.ts + src/services/index.ts + * + * The smithy→SDK compiler and CLI pipeline live in + * `@distilled.cloud/core/codegen`; this script is SpacetimeDB's provider spec. + * + * WASM publish and SQL/name string bodies are not JSON. The OpenAPI converter + * only models application/json request bodies, so those operations are typed + * as a raw `body` payload and this spec stamps `bodyMediaType` on the Http + * trait so the REST protocol sends bytes/text verbatim. + */ +import type { SdkSpec } from "@distilled.cloud/core/codegen/generator"; +import { runGeneratorCli } from "@distilled.cloud/core/codegen/cli"; + +const NULLABLE_TRAIT = "com.distilled.openapi#nullable"; +const ERROR_MATCHERS_TRAIT = "com.distilled.openapi#errorMatchers"; +const RAW_RESPONSE_TRAIT = "com.distilled.openapi#rawResponse"; +const SENSITIVE_TRAIT = "smithy.api#sensitive"; + +const OCTET_STREAM = new Set([ + "POST /v1/database", + "PUT /v1/database/{name_or_identity}", +]); +const TEXT_PLAIN = new Set([ + "POST /v1/database/{name_or_identity}/names", + "POST /v1/database/{name_or_identity}/sql", +]); + +const httpKey = (http: { method?: string; uri?: string }) => + `${http.method ?? ""} ${http.uri ?? ""}`; + +/** SpacetimeDB's provider spec for the shared smithy→SDK compiler. */ +const spacetimedbSpec: SdkSpec = { + nullableTrait: NULLABLE_TRAIT, + errorMatchersTrait: ERROR_MATCHERS_TRAIT, + + extraBindings: [ + { + trait: RAW_RESPONSE_TRAIT, + binding: "rawResponse", + pipe: "T.RawResponse()", + rootPipe: "T.RawResponseRoot()", + }, + ], + + memberTraitPipes: { + [SENSITIVE_TRAIT]: "T.SensitiveValue", + }, + memberTsType: (m, tsRef) => { + if (SENSITIVE_TRAIT in m.traits) { + return `string | Redacted.Redacted${m.nullable ? " | null" : ""}`; + } + if (m.binding === "rawBody" && tsRef(m.target) === "string") { + return `string | Uint8Array | Blob${m.nullable ? " | null" : ""}`; + } + return undefined; + }, + + union: ({ name, caseTargets, tsRef }) => [ + `export type ${name} = ${caseTargets.map(tsRef).join(" | ") || "unknown"};`, + `export const ${name} = /*@__PURE__*/ S.Unknown as any as S.Schema<${name}>;\n`, + ], + + paginationProfiles: { + cursor: { + strategy: "paginateCursor", + itemsFallback: "items", + }, + }, + + structPipes: ({ httpTrait }) => { + if (httpTrait === undefined || httpTrait === null) return []; + const http = { ...(httpTrait as Record) }; + const key = httpKey(http as { method?: string; uri?: string }); + if (OCTET_STREAM.has(key)) { + http.bodyMediaType = "application/octet-stream"; + } else if (TEXT_PLAIN.has(key)) { + http.bodyMediaType = "text/plain"; + } + return [`T.Http(${JSON.stringify(http)})`]; + }, + + operationDecl: { + contextType: "SpacetimeDBOpContext", + commonErrorType: "SpacetimeDBOpError", + commonErrorClasses: ["UnknownSpacetimeDBError"], + protocol: "SpacetimeDBProtocol", + retry: "Retry.Retry", + }, + + sourceNote: ".generated-specs (specs/spec-mirror-spacetimedb)", + + postProcess: (code) => + code.includes("Redacted.Redacted<") + ? code.replace( + `import * as S from "@distilled.cloud/core/schema";\n`, + `import * as S from "@distilled.cloud/core/schema";\nimport * as Redacted from "effect/Redacted";\n`, + ) + : code, +}; + +runGeneratorCli({ + description: "Generate the SpacetimeDB Effect SDK from the Smithy model", + root: `${import.meta.dir}/..`, + patchesDir: false, + spec: () => spacetimedbSpec, +}); diff --git a/packages/spacetimedb/src/credentials.ts b/packages/spacetimedb/src/credentials.ts new file mode 100644 index 0000000000..9a818f0a6b --- /dev/null +++ b/packages/spacetimedb/src/credentials.ts @@ -0,0 +1,73 @@ +/** + * SpacetimeDB credentials — hand-written. + * + * The `Credentials` service holds an *effect* that resolves the current + * credentials on every request (the protocol layer resolves it per request + * on the calling fiber). Auth is an OpenID Connect JWT sent as + * `Authorization: Bearer `. An empty token omits the header so + * `/v1/database` anonymous access still works. + */ +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; +import { ConfigError } from "@distilled.cloud/core/errors"; + +export const DEFAULT_API_BASE_URL = "https://maincloud.spacetimedb.com"; + +export interface Config { + readonly apiKey: Redacted.Redacted; + readonly apiBaseUrl: string; +} + +export class Credentials extends Context.Service< + Credentials, + Effect.Effect +>()("SpacetimeDBCredentials") {} + +/** Layer from a plain token + optional base URL. */ +export const fromApiKey = (config: { + readonly apiKey: string; + readonly apiBaseUrl?: string; +}): Layer.Layer => + Layer.succeed( + Credentials, + Effect.succeed({ + apiKey: Redacted.make(config.apiKey), + apiBaseUrl: config.apiBaseUrl ?? DEFAULT_API_BASE_URL, + }), + ); + +/** Anonymous requests (no Authorization header). */ +export const fromAnonymous = (config?: { + readonly apiBaseUrl?: string; +}): Layer.Layer => + fromApiKey({ apiKey: "", apiBaseUrl: config?.apiBaseUrl }); + +/** + * Reads SPACETIMEDB_TOKEN or SPACETIME_TOKEN (optional — empty is anonymous) + * and SPACETIMEDB_API_BASE_URL or SPACETIMEDB_HOST (optional). + */ +export const CredentialsFromEnv: Layer.Layer = Layer.succeed( + Credentials, + Effect.gen(function* () { + const apiKey = + process.env.SPACETIMEDB_TOKEN ?? process.env.SPACETIME_TOKEN ?? ""; + const apiBaseUrl = + process.env.SPACETIMEDB_API_BASE_URL ?? + process.env.SPACETIMEDB_HOST ?? + process.env.SPACETIME_HOST ?? + DEFAULT_API_BASE_URL; + + if (!apiBaseUrl) { + return yield* new ConfigError({ + message: "SPACETIMEDB_API_BASE_URL environment variable is required", + }); + } + + return { + apiKey: Redacted.make(apiKey), + apiBaseUrl, + }; + }).pipe(Effect.orDie), +); diff --git a/packages/spacetimedb/src/errors.ts b/packages/spacetimedb/src/errors.ts new file mode 100644 index 0000000000..9965aed83d --- /dev/null +++ b/packages/spacetimedb/src/errors.ts @@ -0,0 +1,50 @@ +/** + * SpacetimeDB-specific error types. + * + * Re-exports the common HTTP errors from core and adds the SpacetimeDB + * fallback errors. Note the generated service module additionally defines + * its own per-status matcher classes (BadRequest/NotFound/…) for the + * statuses each operation declares — those share `_tag`s with the core + * classes here, so `catchTag` works against either. + */ +export { + BadGateway, + BadRequest, + Conflict, + ConfigError, + Forbidden, + GatewayTimeout, + InternalServerError, + Locked, + NotFound, + ServiceUnavailable, + TooManyRequests, + Unauthorized, + UnprocessableEntity, + HTTP_STATUS_MAP, + DEFAULT_ERRORS, + API_ERRORS, +} from "@distilled.cloud/core/errors"; +export type { DefaultErrors } from "@distilled.cloud/core/errors"; + +import * as Schema from "effect/Schema"; +import * as Category from "@distilled.cloud/core/category"; + +/** Unknown SpacetimeDB error — returned when nothing else matches the failure. */ +export class UnknownSpacetimeDBError extends Schema.TaggedError()( + "UnknownSpacetimeDBError", + { + code: Schema.optional(Schema.String), + message: Schema.optional(Schema.String), + body: Schema.Unknown, + }, +).pipe(Category.withServerError) {} + +/** Schema parse error wrapper. */ +export class SpacetimeDBParseError extends Schema.TaggedError()( + "SpacetimeDBParseError", + { + body: Schema.Unknown, + cause: Schema.Unknown, + }, +).pipe(Category.withParseError) {} diff --git a/packages/spacetimedb/src/index.ts b/packages/spacetimedb/src/index.ts new file mode 100644 index 0000000000..f2fdd2abde --- /dev/null +++ b/packages/spacetimedb/src/index.ts @@ -0,0 +1,30 @@ +/** + * @distilled.cloud/spacetimedb — SpacetimeDB HTTP management SDK for Effect. + * + * `./services` is generated by `scripts/generate.ts` from the Smithy model + * in `.generated-specs` (written by `scripts/convert.ts` from the OpenAPI + * spec assembled from first-party HTTP docs). Everything else in this folder + * is hand-written. + * + * @example + * ```ts + * import * as SpacetimeDB from "@distilled.cloud/spacetimedb"; + * + * const info = yield* SpacetimeDB.Services.spacetimedb.getDatabase({ + * name_or_identity: "my-db", + * }); + * ``` + */ +export * from "./credentials.ts"; +export * from "./errors.ts"; +export * as T from "./traits.ts"; +export { + SpacetimeDBProtocol, + type SpacetimeDBOpError, + type SpacetimeDBOpContext, +} from "./protocol.ts"; +export { paginateCursor } from "./pagination.ts"; +export * as Retry from "./retry.ts"; +export * as Services from "./services/index.ts"; +export * from "./services/spacetimedb.ts"; +export { BadRequest, NotFound } from "./services/spacetimedb.ts"; diff --git a/packages/spacetimedb/src/pagination.ts b/packages/spacetimedb/src/pagination.ts new file mode 100644 index 0000000000..8b11ca354a --- /dev/null +++ b/packages/spacetimedb/src/pagination.ts @@ -0,0 +1,7 @@ +/** + * SpacetimeDB pagination — hand-written. + * + * The HTTP management API is not cursor-paginated; this re-export is kept so + * the runtime surface matches the other REST SDKs. + */ +export { paginateCursor } from "@distilled.cloud/core/pagination"; diff --git a/packages/spacetimedb/src/protocol.ts b/packages/spacetimedb/src/protocol.ts new file mode 100644 index 0000000000..88a025e75b --- /dev/null +++ b/packages/spacetimedb/src/protocol.ts @@ -0,0 +1,67 @@ +/** + * SpacetimeDBProtocol — hand-written. + * + * SpacetimeDB speaks bearer-authenticated JSON REST (no response envelope), + * with optional anonymous access when no token is configured: + * + * request: credentials → `Authorization: Bearer ` when the token + * is non-empty + base URL (default + * https://maincloud.spacetimedb.com), resolved from the calling + * fiber on every request + * + * response: 2xx JSON is the payload (sensitive members delivered as + * `Redacted`); non-JSON 2xx (logs, PEM, identity hex) is the + * raw text; non-2xx bodies map to the operation's typed error + * classes by status, then the shared HTTP-status classes, then + * {@link UnknownSpacetimeDBError}. + */ +import * as Effect from "effect/Effect"; +import type * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; +import type * as HttpClient from "effect/unstable/http/HttpClient"; +import type * as HttpClientError from "effect/unstable/http/HttpClientError"; +import type * as API from "@distilled.cloud/core/api"; +import { makeRestProtocol } from "@distilled.cloud/core/protocol-rest"; +import type { API_ERRORS, ConfigError } from "@distilled.cloud/core/errors"; +import { Credentials, type Config } from "./credentials.ts"; +import { UnknownSpacetimeDBError } from "./errors.ts"; + +/** + * Error channel shared by every generated SpacetimeDB operation. Generated + * service files annotate operations with + * `API.OperationMethod` + * explicitly so the compiler never infers these back out of the schema + * generics. + */ +export type SpacetimeDBOpError = + | InstanceType<(typeof API_ERRORS)[number]> + | UnknownSpacetimeDBError + | ConfigError + | HttpClientError.HttpClientError; + +/** Context (requirements) shared by every generated SpacetimeDB operation. */ +export type SpacetimeDBOpContext = Credentials | HttpClient.HttpClient; + +export const SpacetimeDBProtocol: Layer.Layer = + makeRestProtocol({ + credentials: Effect.gen(function* () { + const resolve = yield* Credentials; + return yield* resolve; + }), + baseUrl: (creds) => creds.apiBaseUrl, + headers: (creds) => { + const token = Redacted.value(creds.apiKey); + return token === "" ? {} : { Authorization: `Bearer ${token}` }; + }, + unknownError: ({ code, message, body }) => + new UnknownSpacetimeDBError({ + code: + typeof code === "string" + ? code + : code !== undefined + ? String(code) + : undefined, + message, + body, + }), + }); diff --git a/packages/spacetimedb/src/retry.ts b/packages/spacetimedb/src/retry.ts new file mode 100644 index 0000000000..f7333268ed --- /dev/null +++ b/packages/spacetimedb/src/retry.ts @@ -0,0 +1,56 @@ +/** + * SpacetimeDB retry configuration. + * + * Defines the per-SDK `Retry` Context.Service tag that generated operations + * wire into `API.make`. Callers can install a blanket retry policy at the + * layer level and have every SpacetimeDB API call below it pick it up: + * + * @example + * ```ts + * import * as SpacetimeDB from "@distilled.cloud/spacetimedb"; + * + * myEffect.pipe(SpacetimeDB.Retry.transient); + * Effect.provide(myEffect, Layer.succeed(SpacetimeDB.Retry.Retry, customPolicy)); + * ``` + */ +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { + type Policy, + throttlingFactory, + transientFactory, +} from "@distilled.cloud/core/retry"; + +export { + type Options, + type Factory, + type Policy, + makeDefault, + jittered, + capped, + throttlingOptions, + transientOptions, + throttlingFactory, + transientFactory, +} from "@distilled.cloud/core/retry"; + +/** Context tag for configuring retry behavior of SpacetimeDB API calls. */ +export class Retry extends Context.Service()( + "SpacetimeDBRetry", +) {} + +/** Provides a custom retry policy to every SpacetimeDB API call below it. */ +export const policy = (optionsOrFactory: Policy) => + Effect.provide(Layer.succeed(Retry, optionsOrFactory)); + +/** Disables all automatic retries. */ +export const none = Effect.provide( + Layer.succeed(Retry, { while: () => false }), +); + +/** Apply the throttling retry policy (retries throttling errors indefinitely). */ +export const throttling = policy(throttlingFactory); + +/** Apply the transient retry policy (retries all transient errors indefinitely). */ +export const transient = policy(transientFactory); diff --git a/packages/spacetimedb/src/services/index.ts b/packages/spacetimedb/src/services/index.ts new file mode 100644 index 0000000000..4a4b44cf52 --- /dev/null +++ b/packages/spacetimedb/src/services/index.ts @@ -0,0 +1,2 @@ +// AUTO-GENERATED by scripts/generate.ts. Do not edit. +export * as spacetimedb from "./spacetimedb.ts"; diff --git a/packages/spacetimedb/src/services/spacetimedb.ts b/packages/spacetimedb/src/services/spacetimedb.ts new file mode 100644 index 0000000000..a44cc47a28 --- /dev/null +++ b/packages/spacetimedb/src/services/spacetimedb.ts @@ -0,0 +1,891 @@ +// AUTO-GENERATED by scripts/generate.ts from .generated-specs (specs/spec-mirror-spacetimedb). Do not edit. +import * as S from "@distilled.cloud/core/schema"; +import * as Redacted from "effect/Redacted"; +import * as API from "@distilled.cloud/core/api"; +import * as C from "@distilled.cloud/core/category"; +import * as T from "../traits.ts"; +import { + SpacetimeDBProtocol, + type SpacetimeDBOpError, + type SpacetimeDBOpContext, +} from "../protocol.ts"; +import { UnknownSpacetimeDBError } from "../errors.ts"; +import * as Retry from "../retry.ts"; + +export type { SpacetimeDBOpError, SpacetimeDBOpContext }; + +export class BadRequest + extends /*@__PURE__*/ T.applyErrorMatchers( + /*@__PURE__*/ S.TaggedError()("BadRequest", { + code: S.Number, + message: S.String, + }).pipe(C.withBadRequestError), + [{ status: 400 }], + ) {} + +export class NotFound + extends /*@__PURE__*/ T.applyErrorMatchers( + /*@__PURE__*/ S.TaggedError()("NotFound", { + code: S.Number, + message: S.String, + }).pipe(C.withBadRequestError), + [{ status: 404 }], + ) {} + +export interface AddDatabaseNameRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; + body: string | Uint8Array | Blob; +} +export const AddDatabaseNameRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + body: S.String.pipe(T.HttpBody()), + }).pipe( + T.Http({ + method: "POST", + uri: "/v1/database/{name_or_identity}/names", + code: 200, + bodyMediaType: "text/plain", + }), + ), +).annotate({ + identifier: "AddDatabaseNameRequest", +}) as any as S.Schema; + +export interface AddDatabaseNameResponseSuccess { + domain?: string; + database_result?: string; +} +export const AddDatabaseNameResponseSuccess = /*@__PURE__*/ S.suspend(() => + S.Struct({ + domain: S.optional(S.String), + database_result: S.optional(S.String), + }), +).annotate({ + identifier: "AddDatabaseNameResponseSuccess", +}) as any as S.Schema; + +export interface AddDatabaseNameResponse { + Success: AddDatabaseNameResponseSuccess; +} +export const AddDatabaseNameResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + Success: AddDatabaseNameResponseSuccess, + }), +).annotate({ + identifier: "AddDatabaseNameResponse", +}) as any as S.Schema; + +/** JSON array of arguments to the reducer. */ +export type CallReducerRequestBodyList = Array; +export const CallReducerRequestBodyList = /*@__PURE__*/ S.Array( + S.Unknown, +) as any as S.Schema; + +export interface CallReducerRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; + /** The name of the reducer or procedure. */ + reducer: string; + body: CallReducerRequestBodyList; +} +export const CallReducerRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + reducer: S.String.pipe(T.Label()), + body: CallReducerRequestBodyList.pipe(T.HttpBody()), + }).pipe( + T.Http({ + method: "POST", + uri: "/v1/database/{name_or_identity}/call/{reducer}", + code: 200, + }), + ), +).annotate({ + identifier: "CallReducerRequest", +}) as any as S.Schema; + +export type CallReducerResponse = unknown; +export const CallReducerResponse = /*@__PURE__*/ S.suspend(() => + S.Unknown.pipe(T.RawResponseRoot()), +).annotate({ + identifier: "CallReducerResponse", +}) as any as S.Schema; + +export interface CreateIdentityRequest {} +export const CreateIdentityRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({}).pipe(T.Http({ method: "POST", uri: "/v1/identity", code: 200 })), +).annotate({ + identifier: "CreateIdentityRequest", +}) as any as S.Schema; + +export interface CreateIdentityResponse { + identity: string; + token: string | Redacted.Redacted; +} +export const CreateIdentityResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + identity: S.String, + token: S.String.pipe(T.SensitiveValue({})), + }), +).annotate({ + identifier: "CreateIdentityResponse", +}) as any as S.Schema; + +export interface CreateWebsocketTokenRequest {} +export const CreateWebsocketTokenRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({}).pipe( + T.Http({ method: "POST", uri: "/v1/identity/websocket-token", code: 200 }), + ), +).annotate({ + identifier: "CreateWebsocketTokenRequest", +}) as any as S.Schema; + +export interface CreateWebsocketTokenResponse { + token: string | Redacted.Redacted; +} +export const CreateWebsocketTokenResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + token: S.String.pipe(T.SensitiveValue({})), + }), +).annotate({ + identifier: "CreateWebsocketTokenResponse", +}) as any as S.Schema; + +export interface DeleteDatabaseRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; +} +export const DeleteDatabaseRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + }).pipe( + T.Http({ + method: "DELETE", + uri: "/v1/database/{name_or_identity}", + code: 200, + }), + ), +).annotate({ + identifier: "DeleteDatabaseRequest", +}) as any as S.Schema; + +export interface DeleteDatabaseResponse {} +export const DeleteDatabaseResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({}), +).annotate({ + identifier: "DeleteDatabaseResponse", +}) as any as S.Schema; + +export interface ExecuteSqlRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; + body: string | Uint8Array | Blob; +} +export const ExecuteSqlRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + body: S.String.pipe(T.HttpBody()), + }).pipe( + T.Http({ + method: "POST", + uri: "/v1/database/{name_or_identity}/sql", + code: 200, + bodyMediaType: "text/plain", + }), + ), +).annotate({ + identifier: "ExecuteSqlRequest", +}) as any as S.Schema; + +/** JSON-encoded ProductValues conforming to schema. */ +export type ExecuteSqlResponseBodyItemRowsList = Array; +export const ExecuteSqlResponseBodyItemRowsList = /*@__PURE__*/ S.Array( + S.Unknown, +) as any as S.Schema; + +export interface ExecuteSqlResponseBodyItem { + /** JSON-encoded ProductType of the returned rows. */ + schema: unknown; + /** JSON-encoded ProductValues conforming to schema. */ + rows: ExecuteSqlResponseBodyItemRowsList; +} +export const ExecuteSqlResponseBodyItem = /*@__PURE__*/ S.suspend(() => + S.Struct({ + schema: S.Unknown, + rows: ExecuteSqlResponseBodyItemRowsList, + }), +).annotate({ + identifier: "ExecuteSqlResponseBodyItem", +}) as any as S.Schema; + +export type ExecuteSqlResponseBodyList = Array; +export const ExecuteSqlResponseBodyList = /*@__PURE__*/ S.Array( + ExecuteSqlResponseBodyItem, +) as any as S.Schema; + +export type ExecuteSqlResponse = ExecuteSqlResponseBodyList; +export const ExecuteSqlResponse = /*@__PURE__*/ S.suspend(() => + ExecuteSqlResponseBodyList.pipe(T.RawResponseRoot()), +).annotate({ + identifier: "ExecuteSqlResponse", +}) as any as S.Schema; + +export interface GetDatabaseRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; +} +export const GetDatabaseRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + }).pipe( + T.Http({ + method: "GET", + uri: "/v1/database/{name_or_identity}", + code: 200, + }), + ), +).annotate({ + identifier: "GetDatabaseRequest", +}) as any as S.Schema; + +/** The module host type; currently always "wasm". */ +export type GetDatabaseResponseHostType = "wasm"; +export const GetDatabaseResponseHostType = /*@__PURE__*/ S.String; + +export interface GetDatabaseResponse { + /** The Spacetime identity of the database. */ + database_identity: string; + /** The Spacetime identity of the database's owner. */ + owner_identity: string; + /** The module host type; currently always "wasm". */ + host_type: GetDatabaseResponseHostType; + /** Hash of the WASM module with which the database was initialized. */ + initial_program: string; +} +export const GetDatabaseResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + database_identity: S.String, + owner_identity: S.String, + host_type: GetDatabaseResponseHostType, + initial_program: S.String, + }), +).annotate({ + identifier: "GetDatabaseResponse", +}) as any as S.Schema; + +export interface GetDatabaseIdentityRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; +} +export const GetDatabaseIdentityRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + }).pipe( + T.Http({ + method: "GET", + uri: "/v1/database/{name_or_identity}/identity", + code: 200, + }), + ), +).annotate({ + identifier: "GetDatabaseIdentityRequest", +}) as any as S.Schema; + +export type GetDatabaseIdentityResponse = string; +export const GetDatabaseIdentityResponse = /*@__PURE__*/ S.suspend(() => + S.String.pipe(T.RawResponseRoot()), +).annotate({ + identifier: "GetDatabaseIdentityResponse", +}) as any as S.Schema; + +export interface GetDatabaseLogsRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; + /** Number of most-recent log lines to retrieve. */ + num_lines?: number; + /** Whether to continue receiving new logs via a stream. */ + follow?: boolean; +} +export const GetDatabaseLogsRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + num_lines: S.optional(S.Number.pipe(T.Query())), + follow: S.optional(S.Boolean.pipe(T.Query())), + }).pipe( + T.Http({ + method: "GET", + uri: "/v1/database/{name_or_identity}/logs", + code: 200, + }), + ), +).annotate({ + identifier: "GetDatabaseLogsRequest", +}) as any as S.Schema; + +export type GetDatabaseLogsResponse = string; +export const GetDatabaseLogsResponse = /*@__PURE__*/ S.suspend(() => + S.String.pipe(T.RawResponseRoot()), +).annotate({ + identifier: "GetDatabaseLogsResponse", +}) as any as S.Schema; + +export interface GetDatabaseNamesRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; +} +export const GetDatabaseNamesRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + }).pipe( + T.Http({ + method: "GET", + uri: "/v1/database/{name_or_identity}/names", + code: 200, + }), + ), +).annotate({ + identifier: "GetDatabaseNamesRequest", +}) as any as S.Schema; + +export type GetDatabaseNamesResponseNamesList = Array; +export const GetDatabaseNamesResponseNamesList = /*@__PURE__*/ S.Array( + S.String, +) as any as S.Schema; + +export interface GetDatabaseNamesResponse { + names: GetDatabaseNamesResponseNamesList; +} +export const GetDatabaseNamesResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + names: GetDatabaseNamesResponseNamesList, + }), +).annotate({ + identifier: "GetDatabaseNamesResponse", +}) as any as S.Schema; + +export interface GetDatabaseSchemaRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; + /** The version of RawModuleDef to return, e.g. 9. */ + version?: number; +} +export const GetDatabaseSchemaRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + version: S.optional(S.Number.pipe(T.Query())), + }).pipe( + T.Http({ + method: "GET", + uri: "/v1/database/{name_or_identity}/schema", + code: 200, + }), + ), +).annotate({ + identifier: "GetDatabaseSchemaRequest", +}) as any as S.Schema; + +export type GetDatabaseSchemaResponseBodyMap = { + [key: string]: unknown | undefined; +}; +export const GetDatabaseSchemaResponseBodyMap = /*@__PURE__*/ S.Record( + S.String, + S.Unknown, +) as any as S.Schema; + +export type GetDatabaseSchemaResponse = GetDatabaseSchemaResponseBodyMap; +export const GetDatabaseSchemaResponse = /*@__PURE__*/ S.suspend(() => + GetDatabaseSchemaResponseBodyMap.pipe(T.RawResponseRoot()), +).annotate({ + identifier: "GetDatabaseSchemaResponse", +}) as any as S.Schema; + +export interface GetPublicKeyRequest {} +export const GetPublicKeyRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({}).pipe( + T.Http({ method: "GET", uri: "/v1/identity/public-key", code: 200 }), + ), +).annotate({ + identifier: "GetPublicKeyRequest", +}) as any as S.Schema; + +export type GetPublicKeyResponse = string; +export const GetPublicKeyResponse = /*@__PURE__*/ S.suspend(() => + S.String.pipe(T.RawResponseRoot()), +).annotate({ + identifier: "GetPublicKeyResponse", +}) as any as S.Schema; + +export interface ListIdentityDatabasesRequest { + /** A Spacetime identity. */ + identity: string; +} +export const ListIdentityDatabasesRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + identity: S.String.pipe(T.Label()), + }).pipe( + T.Http({ + method: "GET", + uri: "/v1/identity/{identity}/databases", + code: 200, + }), + ), +).annotate({ + identifier: "ListIdentityDatabasesRequest", +}) as any as S.Schema; + +/** Identities of databases owned by the path identity. */ +export type ListIdentityDatabasesResponseIdentitiesList = Array; +export const ListIdentityDatabasesResponseIdentitiesList = + /*@__PURE__*/ S.Array( + S.String, + ) as any as S.Schema; + +export interface ListIdentityDatabasesResponse { + /** Identities of databases owned by the path identity. */ + identities: ListIdentityDatabasesResponseIdentitiesList; +} +export const ListIdentityDatabasesResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + identities: ListIdentityDatabasesResponseIdentitiesList, + }), +).annotate({ + identifier: "ListIdentityDatabasesResponse", +}) as any as S.Schema; + +export interface PingRequest {} +export const PingRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({}).pipe(T.Http({ method: "GET", uri: "/v1/ping", code: 200 })), +).annotate({ identifier: "PingRequest" }) as any as S.Schema; + +export interface PingResponse {} +export const PingResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({}), +).annotate({ identifier: "PingResponse" }) as any as S.Schema; + +export interface PublishDatabaseRequest { + body: string | Uint8Array | Blob; +} +export const PublishDatabaseRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + body: S.String.pipe(T.HttpBody()), + }).pipe( + T.Http({ + method: "POST", + uri: "/v1/database", + code: 200, + bodyMediaType: "application/octet-stream", + }), + ), +).annotate({ + identifier: "PublishDatabaseRequest", +}) as any as S.Schema; + +export type PublishDatabaseResponseSuccessOp = "created" | "updated"; +export const PublishDatabaseResponseSuccessOp = /*@__PURE__*/ S.String; + +export interface PublishDatabaseResponseSuccess { + database_identity: string; + op: PublishDatabaseResponseSuccessOp; +} +export const PublishDatabaseResponseSuccess = /*@__PURE__*/ S.suspend(() => + S.Struct({ + database_identity: S.String, + op: PublishDatabaseResponseSuccessOp, + }), +).annotate({ + identifier: "PublishDatabaseResponseSuccess", +}) as any as S.Schema; + +export interface PublishDatabaseResponse { + Success: PublishDatabaseResponseSuccess; +} +export const PublishDatabaseResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + Success: PublishDatabaseResponseSuccess, + }), +).annotate({ + identifier: "PublishDatabaseResponse", +}) as any as S.Schema; + +/** Names for this database. */ +export type SetDatabaseNamesRequestBodyList = Array; +export const SetDatabaseNamesRequestBodyList = /*@__PURE__*/ S.Array( + S.String, +) as any as S.Schema; + +export interface SetDatabaseNamesRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; + body: SetDatabaseNamesRequestBodyList; +} +export const SetDatabaseNamesRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + body: SetDatabaseNamesRequestBodyList.pipe(T.HttpBody()), + }).pipe( + T.Http({ + method: "PUT", + uri: "/v1/database/{name_or_identity}/names", + code: 200, + }), + ), +).annotate({ + identifier: "SetDatabaseNamesRequest", +}) as any as S.Schema; + +export interface SetDatabaseNamesResponse { + Success: unknown | null; +} +export const SetDatabaseNamesResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + Success: S.NullOr(S.Unknown), + }), +).annotate({ + identifier: "SetDatabaseNamesResponse", +}) as any as S.Schema; + +export interface UpdateDatabaseRequest { + /** Database name or Spacetime identity. */ + name_or_identity: string; + /** Whether to clear any existing data when updating an existing database. */ + clear?: boolean; + body: string | Uint8Array | Blob; +} +export const UpdateDatabaseRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + name_or_identity: S.String.pipe(T.Label()), + clear: S.optional(S.Boolean.pipe(T.Query())), + body: S.String.pipe(T.HttpBody()), + }).pipe( + T.Http({ + method: "PUT", + uri: "/v1/database/{name_or_identity}", + code: 200, + bodyMediaType: "application/octet-stream", + }), + ), +).annotate({ + identifier: "UpdateDatabaseRequest", +}) as any as S.Schema; + +export type UpdateDatabaseResponseSuccessOp = "created" | "updated"; +export const UpdateDatabaseResponseSuccessOp = /*@__PURE__*/ S.String; + +export interface UpdateDatabaseResponseSuccess { + domain?: string | null; + database_identity: string; + op: UpdateDatabaseResponseSuccessOp; +} +export const UpdateDatabaseResponseSuccess = /*@__PURE__*/ S.suspend(() => + S.Struct({ + domain: S.optional(S.NullOr(S.String)), + database_identity: S.String, + op: UpdateDatabaseResponseSuccessOp, + }), +).annotate({ + identifier: "UpdateDatabaseResponseSuccess", +}) as any as S.Schema; + +export interface UpdateDatabaseResponse { + Success: UpdateDatabaseResponseSuccess; +} +export const UpdateDatabaseResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({ + Success: UpdateDatabaseResponseSuccess, + }), +).annotate({ + identifier: "UpdateDatabaseResponse", +}) as any as S.Schema; + +export interface VerifyIdentityRequest { + /** A Spacetime identity. */ + identity: string; +} +export const VerifyIdentityRequest = /*@__PURE__*/ S.suspend(() => + S.Struct({ + identity: S.String.pipe(T.Label()), + }).pipe( + T.Http({ method: "GET", uri: "/v1/identity/{identity}/verify", code: 200 }), + ), +).annotate({ + identifier: "VerifyIdentityRequest", +}) as any as S.Schema; + +export interface VerifyIdentityResponse {} +export const VerifyIdentityResponse = /*@__PURE__*/ S.suspend(() => + S.Struct({}), +).annotate({ + identifier: "VerifyIdentityResponse", +}) as any as S.Schema; + +export type AddDatabaseNameError = SpacetimeDBOpError; +/** Add a new name for this database. Takes as the request body a string containing the new name of the database. */ +export const addDatabaseName: API.OperationMethod< + AddDatabaseNameRequest, + AddDatabaseNameResponse, + AddDatabaseNameError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: AddDatabaseNameRequest, + output: AddDatabaseNameResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type CallReducerError = NotFound | SpacetimeDBOpError; +/** Invoke a reducer or procedure in a database. The caller's identity is passed to the reducer via its ReducerContext. Request body is a JSON array of arguments. */ +export const callReducer: API.OperationMethod< + CallReducerRequest, + CallReducerResponse, + CallReducerError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: CallReducerRequest, + output: CallReducerResponse, + errors: [NotFound, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type CreateIdentityError = SpacetimeDBOpError; +/** Generate a new identity and token. Create a new identity. The returned token is an OpenID Connect compliant JWT signed by this SpacetimeDB host and is not portable to other clusters. */ +export const createIdentity: API.OperationMethod< + CreateIdentityRequest, + CreateIdentityResponse, + CreateIdentityError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: CreateIdentityRequest, + output: CreateIdentityResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type CreateWebsocketTokenError = SpacetimeDBOpError; +/** Generate a short-lived access token for use in untrusted contexts. Generate a short-lived access token which can be used in untrusted contexts, e.g. embedded in URLs. Requires Authorization. */ +export const createWebsocketToken: API.OperationMethod< + CreateWebsocketTokenRequest, + CreateWebsocketTokenResponse, + CreateWebsocketTokenError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: CreateWebsocketTokenRequest, + output: CreateWebsocketTokenResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type DeleteDatabaseError = NotFound | SpacetimeDBOpError; +/** Delete a database. Delete a database. Accessible through the CLI as `spacetime delete`. Deleting a database requires ownership. */ +export const deleteDatabase: API.OperationMethod< + DeleteDatabaseRequest, + DeleteDatabaseResponse, + DeleteDatabaseError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: DeleteDatabaseRequest, + output: DeleteDatabaseResponse, + errors: [NotFound, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type ExecuteSqlError = NotFound | SpacetimeDBOpError; +/** Run a SQL query against a database. Accessible through the CLI as `spacetime sql`. Request body is SQL queries, separated by `;`. Anonymous callers only have access to public tables. */ +export const executeSql: API.OperationMethod< + ExecuteSqlRequest, + ExecuteSqlResponse, + ExecuteSqlError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: ExecuteSqlRequest, + output: ExecuteSqlResponse, + errors: [NotFound, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type GetDatabaseError = NotFound | SpacetimeDBOpError; +/** Get a JSON description of a database. Get a database's identity, owner identity, host type, and a hash of its WASM module. */ +export const getDatabase: API.OperationMethod< + GetDatabaseRequest, + GetDatabaseResponse, + GetDatabaseError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: GetDatabaseRequest, + output: GetDatabaseResponse, + errors: [NotFound, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type GetDatabaseIdentityError = NotFound | SpacetimeDBOpError; +/** Get the identity of a database. Returns a hex string of the specified database's identity. */ +export const getDatabaseIdentity: API.OperationMethod< + GetDatabaseIdentityRequest, + GetDatabaseIdentityResponse, + GetDatabaseIdentityError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: GetDatabaseIdentityRequest, + output: GetDatabaseIdentityResponse, + errors: [NotFound, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type GetDatabaseLogsError = NotFound | SpacetimeDBOpError; +/** Retrieve logs from a database. Viewing logs requires ownership of the database. Accessible through the CLI as `spacetime logs`. Returns text, or streaming text if follow is supplied. */ +export const getDatabaseLogs: API.OperationMethod< + GetDatabaseLogsRequest, + GetDatabaseLogsResponse, + GetDatabaseLogsError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: GetDatabaseLogsRequest, + output: GetDatabaseLogsResponse, + errors: [NotFound, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type GetDatabaseNamesError = NotFound | SpacetimeDBOpError; +/** Get the names this database can be identified by. */ +export const getDatabaseNames: API.OperationMethod< + GetDatabaseNamesRequest, + GetDatabaseNamesResponse, + GetDatabaseNamesError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: GetDatabaseNamesRequest, + output: GetDatabaseNamesResponse, + errors: [NotFound, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type GetDatabaseSchemaError = NotFound | SpacetimeDBOpError; +/** Get the schema for a database. Returns a RawModuleDef in JSON form. Accessible through the CLI as `spacetime describe`. */ +export const getDatabaseSchema: API.OperationMethod< + GetDatabaseSchemaRequest, + GetDatabaseSchemaResponse, + GetDatabaseSchemaError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: GetDatabaseSchemaRequest, + output: GetDatabaseSchemaResponse, + errors: [NotFound, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type GetPublicKeyError = SpacetimeDBOpError; +/** Get the public key used for verifying tokens. Fetches the public key used by the database to verify tokens. Content-Type is application/pem-certificate-chain. */ +export const getPublicKey: API.OperationMethod< + GetPublicKeyRequest, + GetPublicKeyResponse, + GetPublicKeyError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: GetPublicKeyRequest, + output: GetPublicKeyResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type ListIdentityDatabasesError = SpacetimeDBOpError; +/** List databases owned by an identity. */ +export const listIdentityDatabases: API.OperationMethod< + ListIdentityDatabasesRequest, + ListIdentityDatabasesResponse, + ListIdentityDatabasesError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: ListIdentityDatabasesRequest, + output: ListIdentityDatabasesResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type PingError = SpacetimeDBOpError; +/** No-op connectivity probe. Does nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB. */ +export const ping: API.OperationMethod< + PingRequest, + PingResponse, + PingError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: PingRequest, + output: PingResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type PublishDatabaseError = SpacetimeDBOpError; +/** Publish a new database given its module code. Publish a new database with no name. Accessible through the CLI as `spacetime publish`. Request body is a WebAssembly module in the binary format. If no Authorization header is provided, a new anonymous identity owns the database. */ +export const publishDatabase: API.OperationMethod< + PublishDatabaseRequest, + PublishDatabaseResponse, + PublishDatabaseError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: PublishDatabaseRequest, + output: PublishDatabaseResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type SetDatabaseNamesError = SpacetimeDBOpError; +/** Set the list of names for this database. Setting names requires ownership of the database. */ +export const setDatabaseNames: API.OperationMethod< + SetDatabaseNamesRequest, + SetDatabaseNamesResponse, + SetDatabaseNamesError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: SetDatabaseNamesRequest, + output: SetDatabaseNamesResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type UpdateDatabaseError = SpacetimeDBOpError; +/** Publish to a database given its module code. Publish to a database with the specified name or identity. If the name does not exist, creates a new database. Accessible through the CLI as `spacetime publish`. */ +export const updateDatabase: API.OperationMethod< + UpdateDatabaseRequest, + UpdateDatabaseResponse, + UpdateDatabaseError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: UpdateDatabaseRequest, + output: UpdateDatabaseResponse, + errors: [UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); + +export type VerifyIdentityError = BadRequest | SpacetimeDBOpError; +/** Verify an identity and token. Verify the validity of an identity/token pair. 204 if the token is valid and matches the identity; 400 if the token is valid but does not match; 401 if the token is invalid or missing. */ +export const verifyIdentity: API.OperationMethod< + VerifyIdentityRequest, + VerifyIdentityResponse, + VerifyIdentityError, + SpacetimeDBOpContext +> = /*@__PURE__*/ API.make(() => ({ + input: VerifyIdentityRequest, + output: VerifyIdentityResponse, + errors: [BadRequest, UnknownSpacetimeDBError], + protocol: SpacetimeDBProtocol, + retry: Retry.Retry, +})); diff --git a/packages/spacetimedb/src/traits.ts b/packages/spacetimedb/src/traits.ts new file mode 100644 index 0000000000..a063a1c425 --- /dev/null +++ b/packages/spacetimedb/src/traits.ts @@ -0,0 +1,44 @@ +/** + * SpacetimeDB SDK trait surface — hand-written. + * + * Re-exports the generic protocol traits from core (so generated operations + * import everything from one place). SpacetimeDB has no envelope of its own; + * the REST-protocol traits (SensitiveValue / RawResponse) come from + * `core/protocol-rest`. + */ +export { + Body, + Header, + Query, + Label, + Http, + ResponseCode, + HttpBody, + FormDataFile, + KeyDictionary, + UnionCases, + applyErrorMatchers, + getErrorMatchers, + type HttpTrait, + type ErrorMatcher, + bodySymbol, + headerSymbol, + querySymbol, + labelSymbol, + httpSymbol, + responseCodeSymbol, + httpBodySymbol, + formDataFileSymbol, + keyDictionarySymbol, + unionCasesSymbol, + errorMatchersSymbol, +} from "@distilled.cloud/core/trait"; + +export { + SensitiveValue, + RawResponse, + RawResponseRoot, + sensitiveValueSymbol, + rawResponseSymbol, + rawResponseRootSymbol, +} from "@distilled.cloud/core/protocol-rest"; diff --git a/packages/spacetimedb/tsconfig.json b/packages/spacetimedb/tsconfig.json new file mode 100644 index 0000000000..56b16e575f --- /dev/null +++ b/packages/spacetimedb/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "composite": true, + "noEmit": false, + "outDir": "./lib", + "rootDir": "./src", + "paths": { + "@distilled.cloud/core/*": ["../core/src/*"] + } + }, + "references": [{ "path": "../core" }] +} diff --git a/packages/spacetimedb/tsconfig.scripts.json b/packages/spacetimedb/tsconfig.scripts.json new file mode 100644 index 0000000000..4ee702ba8f --- /dev/null +++ b/packages/spacetimedb/tsconfig.scripts.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["scripts/**/*.ts"], + "compilerOptions": { + "composite": true, + "noEmit": false, + "emitDeclarationOnly": true, + "outDir": "./.scripts-types", + "rootDir": "./scripts", + "tsBuildInfoFile": "./.scripts-types/tsconfig.scripts.tsbuildinfo", + "rewriteRelativeImportExtensions": false, + "customConditions": ["bun"] + }, + "references": [{ "path": "../core" }] +} diff --git a/stacks/distilled-submodules/SpecRepos.ts b/stacks/distilled-submodules/SpecRepos.ts index 0c365e4d30..3ae3c75028 100644 --- a/stacks/distilled-submodules/SpecRepos.ts +++ b/stacks/distilled-submodules/SpecRepos.ts @@ -105,6 +105,7 @@ export const SPEC_REPOS: readonly SpecRepo[] = [ { package: "render" }, { package: "resend" }, { package: "sentry" }, + { package: "spacetimedb" }, { package: "squarespace" }, { package: "stripe" }, { package: "supabase" }, diff --git a/stacks/distilled-submodules/spec-repos/spacetimedb/fetch-specs.ts b/stacks/distilled-submodules/spec-repos/spacetimedb/fetch-specs.ts new file mode 100644 index 0000000000..7e55626ce5 --- /dev/null +++ b/stacks/distilled-submodules/spec-repos/spacetimedb/fetch-specs.ts @@ -0,0 +1,908 @@ +#!/usr/bin/env bun +/** + * Assembles SpacetimeDB's HTTP management API into ../specs/. + * + * SpacetimeDB does not publish a first-party OpenAPI file. The official HTTP + * docs live at https://spacetimedb.com/docs/http and are catalogued from + * https://spacetimedb.com/llms.txt. This script snapshots those vendor pages + * (llms.txt + the HTTP markdown derived from each docs page) and writes a + * minimal OpenAPI document covering /v1/database, /v1/identity, and GET + * /v1/ping. Generate never crawls live docs. + * + * Usage: + * bun run fetch-specs.ts + * + * The specs are saved to: + * ../specs/llms.txt + * ../specs/docs/http/*.md + * ../specs/docs/_manifest.json + * ../specs/openapi.json + */ + +import { existsSync, mkdirSync } from "fs"; +import { mkdir, writeFile } from "fs/promises"; +import { dirname, join } from "path"; + +const ORIGIN = "https://spacetimedb.com"; +const LLMS_URL = `${ORIGIN}/llms.txt`; +const SPECS_DIR = "../specs"; +const DOCS_DIR = `${SPECS_DIR}/docs`; +const OUTPUT_PATH = `${SPECS_DIR}/openapi.json`; +const USER_AGENT = "distilled.cloud-spacetimedb-spec-mirror"; + +const HTTP_DOC_PAGES = [ + { path: "/docs/http/database", file: "http/database.md" }, + { path: "/docs/http/identity", file: "http/identity.md" }, + { path: "/docs/http/authorization", file: "http/authorization.md" }, +] as const; + +/** WebSocket upgrade and database-defined catch-all — not the HTTP management API. */ +const SKIPPED_ROUTES = new Set([ + "GET /v1/database/{name_or_identity}/subscribe", + "ANY /v1/database/{name_or_identity}/route/{path}", + "ANY /v1/database/{name_or_identity}/route/{*path}", +]); + +class FetchError extends Error { + constructor( + readonly url: string, + readonly status?: number, + readonly reason?: unknown, + ) { + super( + `${url} — ${status !== undefined ? `HTTP ${status}` : `${reason ?? "network error"}`}`, + ); + } +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function fetchText(url: string, attempts = 8): Promise { + let lastError: FetchError | undefined; + for (let attempt = 0; attempt < attempts; attempt++) { + let error: FetchError; + let retryAfterMs: number | undefined; + try { + const response = await fetch(url, { + headers: { + "user-agent": USER_AGENT, + accept: "text/html, text/plain, text/markdown, */*", + }, + redirect: "follow", + }); + if (response.ok) return await response.text(); + error = new FetchError(url, response.status); + const retryAfter = response.headers.get("retry-after"); + if (retryAfter) { + const seconds = Number(retryAfter); + if (Number.isFinite(seconds) && seconds > 0) { + retryAfterMs = seconds * 1000; + } + } + if (response.status < 500 && response.status !== 429) throw error; + } catch (cause) { + error = + cause instanceof FetchError + ? cause + : new FetchError(url, undefined, cause); + if ( + error.status !== undefined && + error.status < 500 && + error.status !== 429 + ) { + throw error; + } + } + lastError = error; + if (attempt < attempts - 1) { + await sleep(retryAfterMs ?? 750 * 2 ** attempt); + } + } + throw lastError ?? new FetchError(url); +} + +const decodeEntities = (value: string): string => + value + .replace(/ /gi, " ") + .replace(/&/gi, "&") + .replace(/</gi, "<") + .replace(/>/gi, ">") + .replace(/"/gi, '"') + .replace(/'/gi, "'") + .replace(/'/gi, "'") + .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n))); + +const stripTags = (html: string): string => + decodeEntities(html.replace(/<[^>]+>/g, "")) + .replace(/\s+/g, " ") + .trim(); + +/** Deterministic markdown from a Docusaurus HTTP docs page (classes/CDN hashes dropped). */ +function htmlToMarkdown(html: string): string { + const article = html.match(//i)?.[0] ?? html; + let s = article + .replace(//gi, "") + .replace(//gi, "") + .replace(//gi, "") + .replace(//gi, "") + .replace(//gi, "") + .replace(//g, ""); + + s = s.replace( + /]*>\s*]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, + (_, code) => { + const text = decodeEntities(String(code).replace(/<[^>]+>/g, "")).replace( + /\n+$/, + "", + ); + return `\n\n\`\`\`\n${text}\n\`\`\`\n\n`; + }, + ); + + s = s.replace(/]*>([\s\S]*?)<\/h\1>/gi, (_, n, inner) => { + return `\n\n${"#".repeat(Number(n))} ${stripTags(inner)}\n\n`; + }); + + s = s.replace(//gi, (table) => { + const rows = [...table.matchAll(//gi)].map((row) => + [...row[0].matchAll(/]*>([\s\S]*?)<\/t[hd]>/gi)].map((cell) => + stripTags(cell[1] ?? ""), + ), + ); + if (rows.length === 0) return ""; + const header = rows[0]!; + const sep = header.map(() => "---"); + const lines = [ + `| ${header.join(" | ")} |`, + `| ${sep.join(" | ")} |`, + ...rows.slice(1).map((r) => `| ${r.join(" | ")} |`), + ]; + return `\n\n${lines.join("\n")}\n\n`; + }); + + s = s.replace( + /]*>([\s\S]*?)<\/li>/gi, + (_, inner) => `- ${stripTags(inner)}\n`, + ); + s = s.replace( + /]*>([\s\S]*?)<\/p>/gi, + (_, inner) => `\n${stripTags(inner)}\n`, + ); + s = s.replace(//gi, "\n"); + s = s.replace(/<[^>]+>/g, ""); + s = decodeEntities(s); + s = s + .replace(/[ \t]+\n/g, "\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); + return `${s}\n`; +} + +function normalizePath(pathTemplate: string): string { + return pathTemplate + .replace(/[\u200b\u200c\u200d\ufeff]/g, "") + .replace( + /\{([^}/]+)\}/g, + (_, name) => `{${String(name).replace(/^\*/, "")}}`, + ) + .replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}") + .replace(/\/+$/, ""); +} + +function extractRoutes(text: string): string[] { + const routes = new Set(); + const re = /\b(GET|PUT|POST|DELETE|ANY)\s+(\/v1\/[^\s`<\]|]+)/g; + for (const match of text.matchAll(re)) { + const method = match[1]!; + const pathTemplate = normalizePath(match[2]!.replace(/[,.)]+$/, "")); + routes.add(`${method} ${pathTemplate}`); + } + return [...routes].sort(); +} + +const json = (schema: Record) => ({ + content: { "application/json": { schema } }, +}); + +const stringBody = (description: string) => ({ + required: true, + content: { + "application/json": { + schema: { type: "string", description }, + }, + }, +}); + +function assembleOpenApi(): Record { + const nameOrIdentity = { + name: "name_or_identity", + in: "path", + required: true, + description: "Database name or Spacetime identity.", + schema: { type: "string" }, + }; + const identity = { + name: "identity", + in: "path", + required: true, + description: "A Spacetime identity.", + schema: { type: "string" }, + }; + const bearer = [{ bearerAuth: [] }, {}]; + + const publishCreated = { + type: "object", + required: ["Success"], + properties: { + Success: { + type: "object", + required: ["database_identity", "op"], + properties: { + database_identity: { type: "string" }, + op: { type: "string", enum: ["created", "updated"] }, + }, + }, + }, + }; + const publishUpdated = { + type: "object", + required: ["Success"], + properties: { + Success: { + type: "object", + required: ["database_identity", "op"], + properties: { + domain: { type: "string", nullable: true }, + database_identity: { type: "string" }, + op: { type: "string", enum: ["created", "updated"] }, + }, + }, + }, + }; + const permissionDeniedName = { + type: "object", + required: ["PermissionDenied"], + properties: { + PermissionDenied: { + type: "object", + properties: { name: { type: "string" } }, + }, + }, + }; + + return { + openapi: "3.0.3", + info: { + title: "SpacetimeDB HTTP API", + version: "2.0.0", + description: + "HTTP management API for SpacetimeDB (/v1/database, /v1/identity, GET /v1/ping), assembled from https://spacetimedb.com/docs/http and https://spacetimedb.com/llms.txt. SpacetimeDB does not publish a first-party OpenAPI document. WebSocket subscribe and database-defined HTTP handler catch-alls are omitted.", + }, + servers: [{ url: "https://maincloud.spacetimedb.com" }], + tags: [ + { name: "Database", description: "/v1/database management endpoints." }, + { name: "Identity", description: "/v1/identity token endpoints." }, + { name: "System", description: "Top-level connectivity probe." }, + ], + security: bearer, + paths: { + "/v1/ping": { + get: { + operationId: "ping", + tags: ["System"], + summary: "No-op connectivity probe.", + description: + "Does nothing and returns no data. Clients can send requests to this endpoint to determine whether they are able to connect to SpacetimeDB.", + security: [{}], + responses: { "200": { description: "The host is reachable." } }, + }, + }, + "/v1/database": { + post: { + operationId: "publishDatabase", + tags: ["Database"], + summary: "Publish a new database given its module code.", + description: + "Publish a new database with no name. Accessible through the CLI as `spacetime publish`. Request body is a WebAssembly module in the binary format. If no Authorization header is provided, a new anonymous identity owns the database.", + requestBody: stringBody("WebAssembly module in the binary format."), + responses: { + "200": { + description: "The database was published.", + ...json(publishCreated), + }, + }, + }, + }, + "/v1/database/{name_or_identity}": { + parameters: [nameOrIdentity], + put: { + operationId: "updateDatabase", + tags: ["Database"], + summary: "Publish to a database given its module code.", + description: + "Publish to a database with the specified name or identity. If the name does not exist, creates a new database. Accessible through the CLI as `spacetime publish`.", + parameters: [ + { + name: "clear", + in: "query", + description: + "Whether to clear any existing data when updating an existing database.", + schema: { type: "boolean" }, + }, + ], + requestBody: stringBody("WebAssembly module in the binary format."), + responses: { + "200": { + description: "The database was published.", + ...json(publishUpdated), + }, + "401": { + description: + "A database with the given name exists, but the identity is not allowed to edit it.", + ...json(permissionDeniedName), + }, + }, + }, + get: { + operationId: "getDatabase", + tags: ["Database"], + summary: "Get a JSON description of a database.", + description: + "Get a database's identity, owner identity, host type, and a hash of its WASM module.", + responses: { + "200": { + description: "Database description.", + ...json({ + type: "object", + required: [ + "database_identity", + "owner_identity", + "host_type", + "initial_program", + ], + properties: { + database_identity: { + type: "string", + description: "The Spacetime identity of the database.", + }, + owner_identity: { + type: "string", + description: + "The Spacetime identity of the database's owner.", + }, + host_type: { + type: "string", + description: + 'The module host type; currently always "wasm".', + enum: ["wasm"], + }, + initial_program: { + type: "string", + description: + "Hash of the WASM module with which the database was initialized.", + }, + }, + }), + }, + "404": { description: "Database not found." }, + }, + }, + delete: { + operationId: "deleteDatabase", + tags: ["Database"], + summary: "Delete a database.", + description: + "Delete a database. Accessible through the CLI as `spacetime delete`. Deleting a database requires ownership.", + responses: { + "200": { description: "The database was deleted." }, + "204": { description: "The database was deleted." }, + "401": { description: "Caller is not the database owner." }, + "404": { description: "Database not found." }, + }, + }, + }, + "/v1/database/{name_or_identity}/names": { + parameters: [nameOrIdentity], + get: { + operationId: "getDatabaseNames", + tags: ["Database"], + summary: "Get the names this database can be identified by.", + responses: { + "200": { + description: "Database names.", + ...json({ + type: "object", + required: ["names"], + properties: { + names: { type: "array", items: { type: "string" } }, + }, + }), + }, + "404": { description: "Database not found." }, + }, + }, + post: { + operationId: "addDatabaseName", + tags: ["Database"], + summary: "Add a new name for this database.", + description: + "Takes as the request body a string containing the new name of the database.", + requestBody: stringBody("The new name of the database."), + responses: { + "200": { + description: "The name was set.", + ...json({ + type: "object", + required: ["Success"], + properties: { + Success: { + type: "object", + properties: { + domain: { type: "string" }, + database_result: { type: "string" }, + }, + }, + }, + }), + }, + "401": { + description: "The new name exists and the caller cannot edit it.", + ...json({ + type: "object", + required: ["PermissionDenied"], + properties: { + PermissionDenied: { + type: "object", + properties: { domain: { type: "string" } }, + }, + }, + }), + }, + }, + }, + put: { + operationId: "setDatabaseNames", + tags: ["Database"], + summary: "Set the list of names for this database.", + description: "Setting names requires ownership of the database.", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "array", + items: { type: "string" }, + description: "Names for this database.", + }, + }, + }, + }, + responses: { + "200": { + description: "The names were set.", + ...json({ + type: "object", + required: ["Success"], + properties: { Success: { type: "object", nullable: true } }, + }), + }, + "401": { + description: "Caller cannot edit one of the names.", + ...json({ + type: "object", + required: ["PermissionDenied"], + properties: { + PermissionDenied: { type: "object", nullable: true }, + }, + }), + }, + }, + }, + }, + "/v1/database/{name_or_identity}/identity": { + parameters: [nameOrIdentity], + get: { + operationId: "getDatabaseIdentity", + tags: ["Database"], + summary: "Get the identity of a database.", + description: + "Returns a hex string of the specified database's identity.", + responses: { + "200": { + description: "Database identity as a hex string.", + ...json({ type: "string" }), + }, + "404": { description: "Database not found." }, + }, + }, + }, + "/v1/database/{name_or_identity}/call/{reducer}": { + parameters: [ + nameOrIdentity, + { + name: "reducer", + in: "path", + required: true, + description: "The name of the reducer or procedure.", + schema: { type: "string" }, + }, + ], + post: { + operationId: "callReducer", + tags: ["Database"], + summary: "Invoke a reducer or procedure in a database.", + description: + "The caller's identity is passed to the reducer via its ReducerContext. Request body is a JSON array of arguments.", + requestBody: { + required: true, + content: { + "application/json": { + schema: { + type: "array", + items: {}, + description: "JSON array of arguments to the reducer.", + }, + }, + }, + }, + responses: { + "200": { + description: "Reducer or procedure result.", + ...json({}), + }, + "404": { description: "Database or reducer not found." }, + }, + }, + }, + "/v1/database/{name_or_identity}/schema": { + parameters: [nameOrIdentity], + get: { + operationId: "getDatabaseSchema", + tags: ["Database"], + summary: "Get the schema for a database.", + description: + "Returns a RawModuleDef in JSON form. Accessible through the CLI as `spacetime describe`.", + parameters: [ + { + name: "version", + in: "query", + description: "The version of RawModuleDef to return, e.g. 9.", + schema: { type: "integer" }, + }, + ], + responses: { + "200": { + description: "RawModuleDef JSON.", + ...json({ type: "object", additionalProperties: true }), + }, + "404": { description: "Database not found." }, + }, + }, + }, + "/v1/database/{name_or_identity}/logs": { + parameters: [nameOrIdentity], + get: { + operationId: "getDatabaseLogs", + tags: ["Database"], + summary: "Retrieve logs from a database.", + description: + "Viewing logs requires ownership of the database. Accessible through the CLI as `spacetime logs`. Returns text, or streaming text if follow is supplied.", + parameters: [ + { + name: "num_lines", + in: "query", + description: "Number of most-recent log lines to retrieve.", + schema: { type: "integer" }, + }, + { + name: "follow", + in: "query", + description: + "Whether to continue receiving new logs via a stream.", + schema: { type: "boolean" }, + }, + ], + responses: { + "200": { + description: "Log text.", + ...json({ type: "string" }), + }, + "401": { description: "Caller is not the database owner." }, + "404": { description: "Database not found." }, + }, + }, + }, + "/v1/database/{name_or_identity}/sql": { + parameters: [nameOrIdentity], + post: { + operationId: "executeSql", + tags: ["Database"], + summary: "Run a SQL query against a database.", + description: + "Accessible through the CLI as `spacetime sql`. Request body is SQL queries, separated by `;`. Anonymous callers only have access to public tables.", + requestBody: stringBody("SQL queries, separated by `;`."), + responses: { + "200": { + description: "Statement results.", + ...json({ + type: "array", + items: { + type: "object", + required: ["schema", "rows"], + properties: { + schema: { + description: + "JSON-encoded ProductType of the returned rows.", + }, + rows: { + type: "array", + items: {}, + description: + "JSON-encoded ProductValues conforming to schema.", + }, + }, + }, + }), + }, + "404": { description: "Database not found." }, + }, + }, + }, + "/v1/identity": { + post: { + operationId: "createIdentity", + tags: ["Identity"], + summary: "Generate a new identity and token.", + description: + "Create a new identity. The returned token is an OpenID Connect compliant JWT signed by this SpacetimeDB host and is not portable to other clusters.", + security: [{}], + responses: { + "200": { + description: "New identity and token.", + ...json({ + type: "object", + required: ["identity", "token"], + properties: { + identity: { type: "string" }, + token: { type: "string", "x-sensitive": true }, + }, + }), + }, + }, + }, + }, + "/v1/identity/websocket-token": { + post: { + operationId: "createWebsocketToken", + tags: ["Identity"], + summary: + "Generate a short-lived access token for use in untrusted contexts.", + description: + "Generate a short-lived access token which can be used in untrusted contexts, e.g. embedded in URLs. Requires Authorization.", + security: [{ bearerAuth: [] }], + responses: { + "200": { + description: "Short-lived JWT.", + ...json({ + type: "object", + required: ["token"], + properties: { + token: { type: "string", "x-sensitive": true }, + }, + }), + }, + "401": { description: "Missing or invalid token." }, + }, + }, + }, + "/v1/identity/public-key": { + get: { + operationId: "getPublicKey", + tags: ["Identity"], + summary: "Get the public key used for verifying tokens.", + description: + "Fetches the public key used by the database to verify tokens. Content-Type is application/pem-certificate-chain.", + security: [{}], + responses: { + "200": { + description: "PEM certificate chain.", + ...json({ type: "string" }), + }, + }, + }, + }, + "/v1/identity/{identity}/databases": { + parameters: [identity], + get: { + operationId: "listIdentityDatabases", + tags: ["Identity"], + summary: "List databases owned by an identity.", + responses: { + "200": { + description: "Owned database identities.", + ...json({ + type: "object", + required: ["identities"], + properties: { + identities: { + type: "array", + items: { type: "string" }, + description: + "Identities of databases owned by the path identity.", + }, + }, + }), + }, + }, + }, + }, + "/v1/identity/{identity}/verify": { + parameters: [identity], + get: { + operationId: "verifyIdentity", + tags: ["Identity"], + summary: "Verify an identity and token.", + description: + "Verify the validity of an identity/token pair. 204 if the token is valid and matches the identity; 400 if the token is valid but does not match; 401 if the token is invalid or missing.", + security: [{ bearerAuth: [] }], + responses: { + "204": { description: "Token is valid and matches the identity." }, + "400": { + description: "Token is valid but does not match the identity.", + }, + "401": { + description: + "Token is invalid, or no Authorization header was sent.", + }, + }, + }, + }, + }, + components: { + securitySchemes: { + bearerAuth: { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: + "OpenID Connect compliant JWT, such as the token returned from POST /v1/identity. Many /v1/database endpoints also accept anonymous access (no Authorization header).", + }, + }, + }, + }; +} + +const HTTP_METHODS = [ + "get", + "put", + "post", + "delete", + "options", + "head", + "patch", + "trace", +] as const; + +function census(spec: Record) { + let operations = 0; + const keys: string[] = []; + for (const [pathTemplate, item] of Object.entries(spec.paths ?? {})) { + for (const method of HTTP_METHODS) { + if (!item?.[method]) continue; + operations++; + keys.push(`${method.toUpperCase()} ${pathTemplate}`); + } + } + return { + paths: Object.keys(spec.paths ?? {}).length, + operations, + keys: keys.sort(), + }; +} + +if (!existsSync(SPECS_DIR)) { + mkdirSync(SPECS_DIR, { recursive: true }); +} + +async function main() { + console.log(`Fetching ${LLMS_URL}...`); + const llmsTxt = await fetchText(LLMS_URL); + if (!llmsTxt.includes("SpacetimeDB") || !llmsTxt.includes("/docs/http/")) { + throw new Error( + `${LLMS_URL} did not look like SpacetimeDB's docs index — refusing to continue`, + ); + } + await writeFile( + `${SPECS_DIR}/llms.txt`, + llmsTxt.endsWith("\n") ? llmsTxt : `${llmsTxt}\n`, + ); + + await mkdir(DOCS_DIR, { recursive: true }); + + const kept: Array<{ page: string; file: string; markdown: string }> = []; + for (const page of HTTP_DOC_PAGES) { + const url = `${ORIGIN}${page.path}`; + console.log(`Fetching ${url}...`); + const html = await fetchText(url); + if (!html.includes("SpacetimeDB") || html.trim().length < 200) { + throw new Error(`${url} did not look like SpacetimeDB HTTP docs`); + } + const markdown = htmlToMarkdown(html); + if (!markdown.includes("/v1/")) { + throw new Error( + `${url} converted to markdown without /v1/ routes — refusing to continue`, + ); + } + const localPath = join(DOCS_DIR, page.file); + await mkdir(dirname(localPath), { recursive: true }); + await writeFile( + localPath, + markdown.endsWith("\n") ? markdown : `${markdown}\n`, + ); + kept.push({ page: url, file: page.file, markdown }); + } + + await writeFile( + join(DOCS_DIR, "_manifest.json"), + JSON.stringify( + { + source: LLMS_URL, + count: kept.length, + pages: kept.map((page) => ({ + page: page.page, + file: page.file, + })), + }, + null, + 2, + ) + "\n", + ); + + const documented = new Set(); + for (const page of kept) { + for (const route of extractRoutes(page.markdown)) documented.add(route); + } + if (![...documented].some((r) => r.startsWith("POST /v1/database"))) { + throw new Error( + "HTTP docs snapshot is missing POST /v1/database — refusing to continue", + ); + } + + const spec = assembleOpenApi(); + if (typeof spec.openapi !== "string" || spec.paths === undefined) { + throw new Error("assembled document is missing `openapi`/`paths`"); + } + + const c = census(spec); + if (c.operations === 0) { + throw new Error( + "assembled OpenAPI has no operations — refusing to write a gutted spec", + ); + } + + const covered = new Set(c.keys); + const missing: string[] = []; + for (const route of [...documented].sort()) { + if (SKIPPED_ROUTES.has(route)) continue; + if (!covered.has(route)) missing.push(route); + } + if (missing.length > 0) { + throw new Error( + `assembled OpenAPI is missing documented HTTP routes:\n ${missing.join("\n ")}`, + ); + } + + await writeFile(OUTPUT_PATH, JSON.stringify(spec, null, 2) + "\n"); + console.log( + `\n ${OUTPUT_PATH} — OpenAPI ${spec.openapi}, ${c.paths} paths, ${c.operations} operations`, + ); + console.log( + ` skipped ${SKIPPED_ROUTES.size} non-management routes (WebSocket / HTTP handlers)`, + ); + console.log("Done!"); +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/stacks/distilled-submodules/spec-repos/spacetimedb/package.json b/stacks/distilled-submodules/spec-repos/spacetimedb/package.json new file mode 100644 index 0000000000..4c057fbbe8 --- /dev/null +++ b/stacks/distilled-submodules/spec-repos/spacetimedb/package.json @@ -0,0 +1,27 @@ +{ + "name": "spacetimedb-spec", + "private": true, + "type": "module", + "scripts": { + "fetch-specs": "bun run fetch-specs.ts", + "tsc": "tsc --noEmit", + "lint": "oxlint .", + "fmt": "oxfmt --write .", + "fmt:check": "oxfmt --check .", + "check": "bun run tsc && bun run lint && bun run fmt:check" + }, + "workspaces": { + "catalog": { + "@typescript/native-preview": "latest", + "oxfmt": "0.21.0", + "oxlint": "1.36.0" + } + }, + "dependencies": { + "@typescript/native-preview": "catalog:" + }, + "devDependencies": { + "oxfmt": "catalog:", + "oxlint": "catalog:" + } +} diff --git a/stacks/distilled-submodules/spec-repos/spacetimedb/readme.md b/stacks/distilled-submodules/spec-repos/spacetimedb/readme.md new file mode 100644 index 0000000000..64c2059209 --- /dev/null +++ b/stacks/distilled-submodules/spec-repos/spacetimedb/readme.md @@ -0,0 +1,31 @@ +# spec-mirror-spacetimedb + +A git mirror of SpacetimeDB's HTTP management API, assembled from the +first-party docs at [spacetimedb.com/docs/http](https://spacetimedb.com/docs/http) +and the catalog at [spacetimedb.com/llms.txt](https://spacetimedb.com/llms.txt). +SpacetimeDB does not publish a first-party OpenAPI file; the fetch script +snapshots those vendor pages and writes a minimal `specs/openapi.json` covering +`/v1/database`, `/v1/identity`, and `GET /v1/ping`. + +The mirror is updated every 24 hours and is designed to be used as a stable git submodule. + +## Usage as a submodule + +```sh +git submodule add https://github.com/distilled-mirror/spec-mirror-spacetimedb.git +``` + +## Updating specs + +From `.meta/`: + +```sh +bun install +bun run fetch-specs +``` + +--- + +This repository is managed by the `distilled-submodules` Alchemy stack in +[alchemy-run/distilled](https://github.com/alchemy-run/distilled) (`stacks/distilled-submodules`). +Its scaffolding is generated — edit it there, not here. diff --git a/tsconfig.json b/tsconfig.json index 73088c046d..50a79cc46e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -435,6 +435,12 @@ }, { "path": "./packages/modrinth/tsconfig.scripts.json" + }, + { + "path": "./packages/spacetimedb/tsconfig.json" + }, + { + "path": "./packages/spacetimedb/tsconfig.scripts.json" } ] } From e8af2f0a37d1690cc28e1f672733b2b82a07b13d Mon Sep 17 00:00:00 2001 From: "Michael K (Pear)" Date: Mon, 31 Aug 2026 04:16:02 -0400 Subject: [PATCH 2/5] build(spacetimedb): add workspace package to pnpm-lock.yaml --- pnpm-lock.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b33b756a74..a45d7152f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1225,6 +1225,25 @@ importers: specifier: catalog:tooling version: 26.4.1 + packages/spacetimedb: + dependencies: + '@distilled.cloud/core': + specifier: workspace:* + version: link:../core + effect: + specifier: catalog:effect + version: 4.0.0-rc.112 + devDependencies: + '@effect/platform-bun': + specifier: catalog:effect + version: 4.0.0-rc.112(effect@4.0.0-rc.112) + '@types/bun': + specifier: catalog:tooling + version: 1.4.0 + '@types/node': + specifier: catalog:tooling + version: 26.4.0 + packages/stripe: dependencies: '@distilled.cloud/core': From 7d7886f3fa6b51a869bda50d0752db6b72961eff Mon Sep 17 00:00:00 2001 From: "Michael K (Pear)" Date: Mon, 31 Aug 2026 04:26:38 -0400 Subject: [PATCH 3/5] fix: typecheck headers/exports for CI --- packages/spacetimedb/src/protocol.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/spacetimedb/src/protocol.ts b/packages/spacetimedb/src/protocol.ts index 88a025e75b..9aec71b469 100644 --- a/packages/spacetimedb/src/protocol.ts +++ b/packages/spacetimedb/src/protocol.ts @@ -49,9 +49,13 @@ export const SpacetimeDBProtocol: Layer.Layer = return yield* resolve; }), baseUrl: (creds) => creds.apiBaseUrl, - headers: (creds) => { + headers: (creds): Record => { + const headers: Record = {}; const token = Redacted.value(creds.apiKey); - return token === "" ? {} : { Authorization: `Bearer ${token}` }; + if (token !== "") { + headers.Authorization = `Bearer ${token}`; + } + return headers; }, unknownError: ({ code, message, body }) => new UnknownSpacetimeDBError({ From 8dddf893f67997a49f3146cec33a148164438217 Mon Sep 17 00:00:00 2001 From: "Michael K (Pear)" Date: Mon, 7 Sep 2026 07:23:17 -0400 Subject: [PATCH 4/5] chore(spacetimedb): regenerate on the convert-time patch / verbNoun pipeline Rebased on main after #545. Models are produced by the package's convert with patches applied there and operation names normalised to verbNoun; generate only compiles .generated-specs. 18 operation(s). --- packages/spacetimedb/.generated-specs/spacetimedb.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/spacetimedb/.generated-specs/spacetimedb.json b/packages/spacetimedb/.generated-specs/spacetimedb.json index 47d955b5f8..2dfb2c0b74 100644 --- a/packages/spacetimedb/.generated-specs/spacetimedb.json +++ b/packages/spacetimedb/.generated-specs/spacetimedb.json @@ -14,7 +14,8 @@ "id": "UnreferencedShape", "namespace": "*" } - ] + ], + "distilled.finalized": true }, "shapes": { "com.spacetimedb.api#Ping": { From aa92e88d1535fb30d4f6feb576e25d61da2a8144 Mon Sep 17 00:00:00 2001 From: "Michael K (Pear)" Date: Mon, 7 Sep 2026 08:06:33 -0400 Subject: [PATCH 5/5] chore(spacetimedb): regenerate on the stacked base --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a45d7152f0..c9232631c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1206,7 +1206,7 @@ importers: specifier: catalog:tooling version: 26.4.1 - packages/squarespace: + packages/spacetimedb: dependencies: '@distilled.cloud/core': specifier: workspace:* @@ -1225,7 +1225,7 @@ importers: specifier: catalog:tooling version: 26.4.1 - packages/spacetimedb: + packages/squarespace: dependencies: '@distilled.cloud/core': specifier: workspace:* @@ -1239,10 +1239,10 @@ importers: version: 4.0.0-rc.112(effect@4.0.0-rc.112) '@types/bun': specifier: catalog:tooling - version: 1.4.0 + version: 1.4.1 '@types/node': specifier: catalog:tooling - version: 26.4.0 + version: 26.4.1 packages/stripe: dependencies: