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
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.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.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.