TypeScript Sharing

This guide adds sharing features to the TypeScript tutorial app.

New to sharing concepts? Read Sharing & Access Control first.

Prerequisites

Starting checkpoint: typescript/examples/vecelai/doc-checkpoints/03-with-history

Import Sharing Proxy Helpers

app.ts
  memoryServiceConfigFromEnv,
  withMemoryService,
  withProxy,
} from "@chirino/memory-service-vercelai";

const app = express();
app.use(express.text({ type: "*/*" }));

What changed: Checkpoint 06 adds memoryServiceConfigFromEnv(...), withProxy, and a shared memoryServiceConfig used by the sharing endpoints.

Why needed: Sharing operations are delegated to Memory Service through a thin authenticated proxy layer.

Membership Endpoints

Checkpoint 06 adds membership pass-through routes:

app.ts
app.get("/v1/conversations/:conversationId/memberships", async (req, res) => {
  await withProxy(req, res, memoryServiceConfig, (proxy) =>
    proxy.listMemberships(req.params.conversationId),
  );
});

app.post("/v1/conversations/:conversationId/memberships", async (req, res) => {
  await withProxy(req, res, memoryServiceConfig, (proxy) =>
    proxy.createMembership(req.params.conversationId, req.body ?? {}),
  );
});

app.patch(
  "/v1/conversations/:conversationId/memberships/:userId",
  async (req, res) => {
    await withProxy(req, res, memoryServiceConfig, (proxy) =>
      proxy.updateMembership(
        req.params.conversationId,
        req.params.userId,
        req.body ?? {},
      ),
    );
  },
);

app.delete(
  "/v1/conversations/:conversationId/memberships/:userId",
  async (req, res) => {
    await withProxy(req, res, memoryServiceConfig, (proxy) =>
      proxy.deleteMembership(req.params.conversationId, req.params.userId),
    );
  },
);

What changed: The app exposes list/create/update/delete membership endpoints and relays each call to the Memory Service proxy.

Why needed: Access control stays centralized in Memory Service while the app exposes frontend-friendly routes.

Ownership Transfer Endpoints

app.ts
app.get("/v1/ownership-transfers", async (req, res) => {
  await withProxy(req, res, memoryServiceConfig, (proxy) =>
    proxy.listOwnershipTransfers({
      role: (req.query.role as string | undefined) ?? null,
      afterCursor: (req.query.afterCursor as string | undefined) ?? null,
      limit: asNumber(req.query.limit),
    }),
  );
});

app.post("/v1/ownership-transfers", async (req, res) => {
  await withProxy(req, res, memoryServiceConfig, (proxy) =>
    proxy.createOwnershipTransfer(req.body ?? {}),
  );
});

app.post("/v1/ownership-transfers/:transferId/accept", async (req, res) => {
  await withProxy(req, res, memoryServiceConfig, (proxy) =>
    proxy.acceptOwnershipTransfer(req.params.transferId),
  );
});

app.delete("/v1/ownership-transfers/:transferId", async (req, res) => {
  await withProxy(req, res, memoryServiceConfig, (proxy) =>
    proxy.deleteOwnershipTransfer(req.params.transferId),
  );
});

What changed: The app adds list/create/accept/delete ownership transfer endpoints.

Why needed: Ownership transfer is multi-step, so clients need all lifecycle routes (list/create/accept/delete).

Testing Membership Management

Define a token helper for multiple users:

function get-token() {
  local username=${1:-bob}
  local password=${2:-$username}
  curl -sSfX POST http://localhost:8081/realms/memory-service/protocol/openid-connect/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "client_id=memory-service-client" \
    -d "client_secret=change-me" \
    -d "grant_type=password" \
    -d "username=$username" \
    -d "password=$password" \
    | jq -r '.access_token'
}

Create a conversation as bob:

curl -sSfX POST http://localhost:9090/chat/d2c7e5a1-4b9d-4f8c-9e12-6c3b1a7f6444 \
  -H "Authorization: Bearer $(get-token bob bob)" \
  -H "Content-Type: text/plain" \
  -d "Hello, starting a sharing test conversation."

Example output:

I am a TypeScript memory-service demo agent.

List memberships:

curl -sSfX GET http://localhost:9090/v1/conversations/d2c7e5a1-4b9d-4f8c-9e12-6c3b1a7f6444/memberships \
  -H "Authorization: Bearer $(get-token bob bob)" | jq

Example output:

{
  "afterCursor": null,
  "data": [
    {
      "conversationId": "d2c7e5a1-4b9d-4f8c-9e12-6c3b1a7f6444",
      "userId": "bob",
      "accessLevel": "owner",
      "createdAt": "2026-03-06T14:59:41.670039Z"
    }
  ]
}

Add alice as a writer:

curl -sSfX POST http://localhost:9090/v1/conversations/d2c7e5a1-4b9d-4f8c-9e12-6c3b1a7f6444/memberships \
  -H "Authorization: Bearer $(get-token bob bob)" \
  -H "Content-Type: application/json" \
  -d '{"userId":"alice","accessLevel":"writer"}' | jq

Example output:

{
  "conversationId": "d2c7e5a1-4b9d-4f8c-9e12-6c3b1a7f6444",
  "userId": "alice",
  "accessLevel": "writer",
  "createdAt": "2026-03-06T14:59:41.714772Z"
}

Testing Ownership Transfers

Initiate an ownership transfer:

curl -sSfX POST http://localhost:9090/v1/ownership-transfers \
  -H "Authorization: Bearer $(get-token bob bob)" \
  -H "Content-Type: application/json" \
  -d '{
    "conversationId": "d2c7e5a1-4b9d-4f8c-9e12-6c3b1a7f6444",
    "newOwnerUserId": "alice"
  }' | jq

List transfers for the recipient:

curl -sSfX GET "http://localhost:9090/v1/ownership-transfers?role=recipient" \
  -H "Authorization: Bearer $(get-token alice alice)" | jq

Accept a transfer:

curl -sSfX POST http://localhost:9090/v1/ownership-transfers/<transfer-id>/accept \
  -H "Authorization: Bearer $(get-token alice alice)"

Decline or cancel a transfer:

curl -sSfX DELETE http://localhost:9090/v1/ownership-transfers/<transfer-id> \
  -H "Authorization: Bearer $(get-token alice alice)"

Next Steps

Continue to Indexing and Search to add searchable indexed content.